fix: address review issues in PR #2829 webhook intentional deletion guard

5 issues found by review agents and fixed:

1. Intentional deletion guard was at top of process_event, skipping ALL
   cleanup (subscription URLs, server counts, expired marking). Moved
   check into _handle_user_deleted so cleanup still runs but re-creation
   is suppressed via subscription_still_valid=False.

2. Variable shadowing: telegram_id loop var in any() generator shadowed
   the outer telegram_id local. Renamed to tid/uid.

3. No hard cap on in-memory dicts: added _MAX_INTENTIONAL_ENTRIES=10000
   with early return in mark_intentional_panel_deletion.

4. mark_intentional_panel_deletion called inside for-loop with single
   UUID — race window if webhook from first delete arrives before
   second UUID is marked. Moved to before the loop with all UUIDs.

5. Tests: @pytest.mark.anyio → @pytest.mark.anyio('asyncio') for
   consistency. Replaced process_event(db=None) test with direct
   mark+detect unit tests + hard cap test.
This commit is contained in:
Fringg
2026-04-02 06:34:35 +03:00
parent 6f6b9fa039
commit 977950b97f
3 changed files with 56 additions and 32 deletions
+21 -12
View File
@@ -54,7 +54,7 @@ def _signature(body: bytes) -> str:
return hmac.new(secret.encode('utf-8'), body, hashlib.sha256).hexdigest()
@pytest.mark.anyio
@pytest.mark.anyio('asyncio')
async def test_remnawave_webhook_accepts_event_without_scope(monkeypatch: pytest.MonkeyPatch) -> None:
bot = AsyncMock()
process_event = AsyncMock(return_value=True)
@@ -90,7 +90,7 @@ async def test_remnawave_webhook_accepts_event_without_scope(monkeypatch: pytest
process_event.assert_awaited_once_with(None, 'user.modified', {'uuid': 'user-123'})
@pytest.mark.anyio
@pytest.mark.anyio('asyncio')
async def test_remnawave_webhook_rejects_payload_without_event() -> None:
bot = AsyncMock()
payload = {'data': {'uuid': 'user-123'}}
@@ -111,20 +111,29 @@ async def test_remnawave_webhook_rejects_payload_without_event() -> None:
assert json.loads(response.body.decode('utf-8')) == {'status': 'error', 'reason': 'missing_event'}
@pytest.mark.anyio
async def test_process_event_skips_intentional_admin_user_deleted() -> None:
bot = AsyncMock()
service = RemnaWaveWebhookService(bot)
def test_intentional_panel_deletion_guard_marks_and_detects() -> None:
"""Verify that mark + is_intentional round-trip works correctly."""
RemnaWaveWebhookService.mark_intentional_panel_deletion(
panel_uuids=['panel-user-123'],
telegram_id=8368498066,
)
processed = await service.process_event(
None,
'user.deleted',
{'uuid': 'panel-user-123', 'telegramId': 8368498066},
assert RemnaWaveWebhookService._is_intentional_panel_deletion_event(
{'uuid': 'panel-user-123', 'telegramId': 8368498066}
)
assert processed is True
# Unknown UUID should not match
assert not RemnaWaveWebhookService._is_intentional_panel_deletion_event(
{'uuid': 'unknown-uuid', 'telegramId': 99999}
)
def test_intentional_panel_deletion_guard_respects_hard_cap(monkeypatch: pytest.MonkeyPatch) -> None:
"""Verify that the guard stops accepting entries after hitting the cap."""
monkeypatch.setattr(RemnaWaveWebhookService, '_MAX_INTENTIONAL_ENTRIES', 3)
RemnaWaveWebhookService.mark_intentional_panel_deletion(panel_uuids=['a', 'b', 'c'])
# 3 entries — at capacity
RemnaWaveWebhookService.mark_intentional_panel_deletion(panel_uuids=['d'])
# 'd' should NOT be stored (cap reached)
assert 'd' not in RemnaWaveWebhookService._intentional_panel_deletions_by_uuid