Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions tests/notifications/test_backend_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,140 @@ async def test_defaults_to_first_page(self):
assert http_client.get.await_args.kwargs["params"] == {"skip": 0, "limit": 100}


class TestFetchGroupPostNotificationTargets:
@pytest.mark.asyncio
async def test_returns_parsed_targets(self):
post_id = uuid4()
device_id = uuid4()
response = _json_response(
{
"post_id": str(post_id),
"group_id": str(uuid4()),
"author_id": str(uuid4()),
"title": "Sangha",
"body": "Alice shared a new post",
"recipients": [
{
"user_id": str(uuid4()),
"push_devices": [
{"id": str(device_id), "token": "token-1", "platform": "ios"}
],
}
],
"skip": 100,
"limit": 50,
"total": 120,
"has_more": False,
}
)
client_patch, http_client = _patch_async_client(response)

with client_patch, _patch_config():
targets = await backend_client.fetch_group_post_notification_targets(
post_id=post_id,
skip=100,
limit=50,
)

assert targets.total == 120
assert targets.recipients[0].push_devices[0].id == device_id
assert http_client.get.await_args.args[0] == (
f"http://backend.test/internal/group-post-notification-targets/{post_id}"
)
assert http_client.get.await_args.kwargs["params"] == {"skip": 100, "limit": 50}

@pytest.mark.asyncio
async def test_defaults_to_first_page(self):
post_id = uuid4()
response = _json_response(
{
"post_id": str(post_id),
"group_id": str(uuid4()),
"author_id": str(uuid4()),
"title": "Sangha",
"body": "Alice shared a new post",
"recipients": [],
"skip": 0,
"limit": 100,
"total": 0,
"has_more": False,
}
)
client_patch, http_client = _patch_async_client(response)

with client_patch, _patch_config():
await backend_client.fetch_group_post_notification_targets(post_id=post_id)

assert http_client.get.await_args.kwargs["params"] == {"skip": 0, "limit": 100}


class TestFetchEventNotificationTargets:
@pytest.mark.asyncio
async def test_returns_parsed_targets(self):
event_id = uuid4()
device_id = uuid4()
response = _json_response(
{
"event_id": str(event_id),
"group_id": str(uuid4()),
"author_id": str(uuid4()),
"title": "Sangha",
"body": "Full Moon Meditation",
"recipients": [
{
"user_id": str(uuid4()),
"push_devices": [
{"id": str(device_id), "token": "token-1", "platform": "ios"}
],
}
],
"skip": 100,
"limit": 50,
"total": 120,
"has_more": False,
}
)
client_patch, http_client = _patch_async_client(response)

with client_patch, _patch_config():
targets = await backend_client.fetch_event_notification_targets(
event_id=event_id,
skip=100,
limit=50,
)

assert targets.total == 120
assert targets.recipients[0].push_devices[0].id == device_id
assert http_client.get.await_args.args[0] == (
f"http://backend.test/internal/event-notification-targets/{event_id}"
)
assert http_client.get.await_args.kwargs["params"] == {"skip": 100, "limit": 50}

@pytest.mark.asyncio
async def test_defaults_to_first_page(self):
event_id = uuid4()
response = _json_response(
{
"event_id": str(event_id),
"group_id": str(uuid4()),
"author_id": str(uuid4()),
"title": "Sangha",
"body": "Full Moon Meditation",
"recipients": [],
"skip": 0,
"limit": 100,
"total": 0,
"has_more": False,
}
)
client_patch, http_client = _patch_async_client(response)

with client_patch, _patch_config():
await backend_client.fetch_event_notification_targets(event_id=event_id)

assert http_client.get.await_args.kwargs["params"] == {"skip": 0, "limit": 100}


class TestFetchVerseOfDayNotificationTargets:
@pytest.mark.asyncio
async def test_returns_parsed_targets(self):
Expand Down
220 changes: 220 additions & 0 deletions tests/notifications/test_event_notification_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"""Tests for event notification SQS consumer."""
import json
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest

from worker_api.notifications.schemas import (
EventNotificationRecipient,
EventNotificationTargetsResponse,
EventPushDeviceTarget,
)
from worker_api.notifications.services.event_notification_consumer import (
TransientEventNotificationError,
process_event_notification_message,
)
from worker_api.notifications.services.push.fcm_client import PermanentPushTokenError


def _targets(*, event_id, devices):
return EventNotificationTargetsResponse(
event_id=event_id,
group_id=uuid4(),
author_id=uuid4(),
title="Sangha",
body="Full Moon Meditation",
recipients=[
EventNotificationRecipient(
user_id=uuid4(),
push_devices=devices,
)
],
skip=0,
limit=100,
total=1,
has_more=False,
)


def _body(event_id):
return json.dumps(
{
"event_type": "EVENT_CREATED",
"version": 1,
"event_id": str(event_id),
}
)


class TestProcessEventNotificationMessage:
@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
async def test_deletes_malformed_message(self, mock_delete):
await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": "not-json"}
)
mock_delete.assert_called_once_with("r1")

@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
@patch(
"worker_api.notifications.services.event_notification_consumer._fetch_all_targets",
new_callable=AsyncMock,
)
@patch("worker_api.notifications.services.event_notification_consumer.get_bool", return_value=True)
async def test_deletes_when_event_not_found(self, _get_bool, mock_fetch, mock_delete):
from fastapi import HTTPException

mock_fetch.side_effect = HTTPException(status_code=404, detail="not found")
event_id = uuid4()
await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": _body(event_id)}
)
mock_delete.assert_called_once_with("r1")

@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
@patch(
"worker_api.notifications.services.event_notification_consumer.send_event_push_notification",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._fetch_all_targets",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer.is_push_configured",
return_value=True,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._already_sent",
return_value=False,
)
@patch("worker_api.notifications.services.event_notification_consumer._mark_sent")
@patch("worker_api.notifications.services.event_notification_consumer.get_int", return_value=5)
@patch("worker_api.notifications.services.event_notification_consumer.get_bool", return_value=True)
async def test_sends_and_deletes_on_success(
self, _get_bool, _get_int, mock_mark, _already, _configured, mock_fetch, mock_send, mock_delete,
):
event_id = uuid4()
device = EventPushDeviceTarget(id=uuid4(), token="tok", platform="android")
mock_fetch.return_value = _targets(event_id=event_id, devices=[device])

await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": _body(event_id)}
)

mock_send.assert_awaited_once()
mock_mark.assert_called_once()
mock_delete.assert_called_once_with("r1")

@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
@patch(
"worker_api.notifications.services.event_notification_consumer.deactivate_push_device",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer.send_event_push_notification",
new_callable=AsyncMock,
side_effect=PermanentPushTokenError("gone"),
)
@patch(
"worker_api.notifications.services.event_notification_consumer._fetch_all_targets",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer.is_push_configured",
return_value=True,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._already_sent",
return_value=False,
)
@patch("worker_api.notifications.services.event_notification_consumer._mark_sent")
@patch("worker_api.notifications.services.event_notification_consumer.get_int", return_value=5)
@patch("worker_api.notifications.services.event_notification_consumer.get_bool", return_value=True)
async def test_permanent_token_deactivates_and_deletes(
self, _get_bool, _get_int, mock_mark, _already, _configured, mock_fetch, mock_send, mock_deactivate, mock_delete,
):
event_id = uuid4()
device = EventPushDeviceTarget(id=uuid4(), token="tok", platform="android")
mock_fetch.return_value = _targets(event_id=event_id, devices=[device])

await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": _body(event_id)}
)

mock_deactivate.assert_awaited_once_with(push_device_id=device.id)
mock_mark.assert_called_once()
mock_delete.assert_called_once_with("r1")

@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
@patch(
"worker_api.notifications.services.event_notification_consumer.send_event_push_notification",
new_callable=AsyncMock,
side_effect=RuntimeError("temporary"),
)
@patch(
"worker_api.notifications.services.event_notification_consumer._fetch_all_targets",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer.is_push_configured",
return_value=True,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._already_sent",
return_value=False,
)
@patch("worker_api.notifications.services.event_notification_consumer.get_int", return_value=5)
@patch("worker_api.notifications.services.event_notification_consumer.get_bool", return_value=True)
async def test_transient_failure_leaves_message(
self, _get_bool, _get_int, _already, _configured, mock_fetch, mock_send, mock_delete,
):
event_id = uuid4()
device = EventPushDeviceTarget(id=uuid4(), token="tok", platform="android")
mock_fetch.return_value = _targets(event_id=event_id, devices=[device])

with pytest.raises(TransientEventNotificationError):
await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": _body(event_id)}
)

mock_delete.assert_not_called()

@pytest.mark.asyncio
@patch("worker_api.notifications.services.event_notification_consumer.delete_event_notification_message")
@patch(
"worker_api.notifications.services.event_notification_consumer.send_event_push_notification",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._fetch_all_targets",
new_callable=AsyncMock,
)
@patch(
"worker_api.notifications.services.event_notification_consumer.is_push_configured",
return_value=True,
)
@patch(
"worker_api.notifications.services.event_notification_consumer._already_sent",
return_value=True,
)
@patch("worker_api.notifications.services.event_notification_consumer.get_int", return_value=5)
@patch("worker_api.notifications.services.event_notification_consumer.get_bool", return_value=True)
async def test_skips_already_sent_devices(
self, _get_bool, _get_int, _already, _configured, mock_fetch, mock_send, mock_delete,
):
event_id = uuid4()
device = EventPushDeviceTarget(id=uuid4(), token="tok", platform="android")
mock_fetch.return_value = _targets(event_id=event_id, devices=[device])

await process_event_notification_message(
{"ReceiptHandle": "r1", "Body": _body(event_id)}
)

mock_send.assert_not_called()
mock_delete.assert_called_once_with("r1")
Loading
Loading