From 8a77a8b251a56245c7a6c679165ef1be01250ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Casaj=C3=BAs?= Date: Fri, 7 Jun 2024 15:36:18 +0200 Subject: [PATCH] Create jobs to trigger sending all alias as create events (#2126) * Create jobs to trigger sending all alias as create events * Set events in past tense * fix test * Removed debug log * Log messages --- app/alias_utils.py | 9 +++- app/api/views/alias.py | 2 + app/config.py | 1 + app/events/generated/event_pb2.py | 46 +++++++++++-------- app/events/generated/event_pb2.pyi | 22 ++++----- app/handler/unsubscribe_handler.py | 1 + app/jobs/event_jobs.py | 40 ++++++++++++++++ app/subscription_webhook.py | 4 +- job_runner.py | 9 +++- poetry.lock | 28 ++++++----- proto/event.proto | 14 +++--- tests/jobs/test_send_alias_creation_events.py | 46 +++++++++++++++++++ tests/utils.py | 15 +++++- 13 files changed, 180 insertions(+), 57 deletions(-) create mode 100644 app/jobs/event_jobs.py create mode 100644 tests/jobs/test_send_alias_creation_events.py diff --git a/app/alias_utils.py b/app/alias_utils.py index d4cafa18..99f291eb 100644 --- a/app/alias_utils.py +++ b/app/alias_utils.py @@ -26,7 +26,11 @@ from app.email_utils import ( ) from app.errors import AliasInTrashError from app.events.event_dispatcher import EventDispatcher -from app.events.generated.event_pb2 import AliasDeleted, AliasStatusChange, EventContent +from app.events.generated.event_pb2 import ( + AliasDeleted, + AliasStatusChanged, + EventContent, +) from app.log import LOG from app.models import ( Alias, @@ -468,9 +472,10 @@ def transfer_alias(alias, new_user, new_mailboxes: [Mailbox]): def change_alias_status(alias: Alias, enabled: bool, commit: bool = False): + LOG.i(f"Changing alias {alias} enabled to {enabled}") alias.enabled = enabled - event = AliasStatusChange( + event = AliasStatusChanged( alias_id=alias.id, alias_email=alias.email, enabled=enabled ) EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event)) diff --git a/app/api/views/alias.py b/app/api/views/alias.py index bd4587cf..50f455dc 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -25,6 +25,7 @@ from app.errors import ( ErrAddressInvalid, ) from app.extensions import limiter +from app.log import LOG from app.models import Alias, Contact, Mailbox, AliasMailbox @@ -185,6 +186,7 @@ def toggle_alias(alias_id): return jsonify(error="Forbidden"), 403 alias_utils.change_alias_status(alias, enabled=not alias.enabled) + LOG.i(f"User {user} changed alias {alias} enabled status to {alias.enabled}") Session.commit() return jsonify(enabled=alias.enabled), 200 diff --git a/app/config.py b/app/config.py index 325b475f..9a7fb62b 100644 --- a/app/config.py +++ b/app/config.py @@ -281,6 +281,7 @@ JOB_DELETE_MAILBOX = "delete-mailbox" JOB_DELETE_DOMAIN = "delete-domain" JOB_SEND_USER_REPORT = "send-user-report" JOB_SEND_PROTON_WELCOME_1 = "proton-welcome-1" +JOB_SEND_ALIAS_CREATION_EVENTS = "send-alias-creation-events" # for pagination PAGE_LIMIT = 20 diff --git a/app/events/generated/event_pb2.py b/app/events/generated/event_pb2.py index af494710..a805c62c 100644 --- a/app/events/generated/event_pb2.py +++ b/app/events/generated/event_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: event.proto -# Protobuf Python Version: 5.26.1 +# Protobuf Python Version: 5.27.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 0, + '', + 'event.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,27 +24,27 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12simplelogin_events\"\'\n\x0eUserPlanChange\x12\x15\n\rplan_end_time\x18\x01 \x01(\r\"\r\n\x0bUserDeleted\"Z\n\x0c\x41liasCreated\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x12\n\nalias_note\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\"K\n\x11\x41liasStatusChange\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\"5\n\x0c\x41liasDeleted\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\"B\n\x0f\x41liasCreateList\x12/\n\x05\x65vent\x18\x01 \x03(\x0b\x32 .simplelogin_events.AliasCreated\"\x90\x03\n\x0c\x45ventContent\x12>\n\x10user_plan_change\x18\x01 \x01(\x0b\x32\".simplelogin_events.UserPlanChangeH\x00\x12\x37\n\x0cuser_deleted\x18\x02 \x01(\x0b\x32\x1f.simplelogin_events.UserDeletedH\x00\x12\x39\n\ralias_created\x18\x03 \x01(\x0b\x32 .simplelogin_events.AliasCreatedH\x00\x12\x44\n\x13\x61lias_status_change\x18\x04 \x01(\x0b\x32%.simplelogin_events.AliasStatusChangeH\x00\x12\x39\n\ralias_deleted\x18\x05 \x01(\x0b\x32 .simplelogin_events.AliasDeletedH\x00\x12@\n\x11\x61lias_create_list\x18\x06 \x01(\x0b\x32#.simplelogin_events.AliasCreateListH\x00\x42\t\n\x07\x63ontent\"y\n\x05\x45vent\x12\x0f\n\x07user_id\x18\x01 \x01(\r\x12\x18\n\x10\x65xternal_user_id\x18\x02 \x01(\t\x12\x12\n\npartner_id\x18\x03 \x01(\r\x12\x31\n\x07\x63ontent\x18\x04 \x01(\x0b\x32 .simplelogin_events.EventContentb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12simplelogin_events\"(\n\x0fUserPlanChanged\x12\x15\n\rplan_end_time\x18\x01 \x01(\r\"\r\n\x0bUserDeleted\"Z\n\x0c\x41liasCreated\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x12\n\nalias_note\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\"L\n\x12\x41liasStatusChanged\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\"5\n\x0c\x41liasDeleted\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\"D\n\x10\x41liasCreatedList\x12\x30\n\x06\x65vents\x18\x01 \x03(\x0b\x32 .simplelogin_events.AliasCreated\"\x93\x03\n\x0c\x45ventContent\x12?\n\x10user_plan_change\x18\x01 \x01(\x0b\x32#.simplelogin_events.UserPlanChangedH\x00\x12\x37\n\x0cuser_deleted\x18\x02 \x01(\x0b\x32\x1f.simplelogin_events.UserDeletedH\x00\x12\x39\n\ralias_created\x18\x03 \x01(\x0b\x32 .simplelogin_events.AliasCreatedH\x00\x12\x45\n\x13\x61lias_status_change\x18\x04 \x01(\x0b\x32&.simplelogin_events.AliasStatusChangedH\x00\x12\x39\n\ralias_deleted\x18\x05 \x01(\x0b\x32 .simplelogin_events.AliasDeletedH\x00\x12\x41\n\x11\x61lias_create_list\x18\x06 \x01(\x0b\x32$.simplelogin_events.AliasCreatedListH\x00\x42\t\n\x07\x63ontent\"y\n\x05\x45vent\x12\x0f\n\x07user_id\x18\x01 \x01(\r\x12\x18\n\x10\x65xternal_user_id\x18\x02 \x01(\t\x12\x12\n\npartner_id\x18\x03 \x01(\r\x12\x31\n\x07\x63ontent\x18\x04 \x01(\x0b\x32 .simplelogin_events.EventContentb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'event_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_USERPLANCHANGE']._serialized_start=35 - _globals['_USERPLANCHANGE']._serialized_end=74 - _globals['_USERDELETED']._serialized_start=76 - _globals['_USERDELETED']._serialized_end=89 - _globals['_ALIASCREATED']._serialized_start=91 - _globals['_ALIASCREATED']._serialized_end=181 - _globals['_ALIASSTATUSCHANGE']._serialized_start=183 - _globals['_ALIASSTATUSCHANGE']._serialized_end=258 - _globals['_ALIASDELETED']._serialized_start=260 - _globals['_ALIASDELETED']._serialized_end=313 - _globals['_ALIASCREATELIST']._serialized_start=315 - _globals['_ALIASCREATELIST']._serialized_end=381 - _globals['_EVENTCONTENT']._serialized_start=384 - _globals['_EVENTCONTENT']._serialized_end=784 - _globals['_EVENT']._serialized_start=786 - _globals['_EVENT']._serialized_end=907 + _globals['_USERPLANCHANGED']._serialized_start=35 + _globals['_USERPLANCHANGED']._serialized_end=75 + _globals['_USERDELETED']._serialized_start=77 + _globals['_USERDELETED']._serialized_end=90 + _globals['_ALIASCREATED']._serialized_start=92 + _globals['_ALIASCREATED']._serialized_end=182 + _globals['_ALIASSTATUSCHANGED']._serialized_start=184 + _globals['_ALIASSTATUSCHANGED']._serialized_end=260 + _globals['_ALIASDELETED']._serialized_start=262 + _globals['_ALIASDELETED']._serialized_end=315 + _globals['_ALIASCREATEDLIST']._serialized_start=317 + _globals['_ALIASCREATEDLIST']._serialized_end=385 + _globals['_EVENTCONTENT']._serialized_start=388 + _globals['_EVENTCONTENT']._serialized_end=791 + _globals['_EVENT']._serialized_start=793 + _globals['_EVENT']._serialized_end=914 # @@protoc_insertion_point(module_scope) diff --git a/app/events/generated/event_pb2.pyi b/app/events/generated/event_pb2.pyi index 121ffd4b..d1793eed 100644 --- a/app/events/generated/event_pb2.pyi +++ b/app/events/generated/event_pb2.pyi @@ -5,7 +5,7 @@ from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Map DESCRIPTOR: _descriptor.FileDescriptor -class UserPlanChange(_message.Message): +class UserPlanChanged(_message.Message): __slots__ = ("plan_end_time",) PLAN_END_TIME_FIELD_NUMBER: _ClassVar[int] plan_end_time: int @@ -27,7 +27,7 @@ class AliasCreated(_message.Message): enabled: bool def __init__(self, alias_id: _Optional[int] = ..., alias_email: _Optional[str] = ..., alias_note: _Optional[str] = ..., enabled: bool = ...) -> None: ... -class AliasStatusChange(_message.Message): +class AliasStatusChanged(_message.Message): __slots__ = ("alias_id", "alias_email", "enabled") ALIAS_ID_FIELD_NUMBER: _ClassVar[int] ALIAS_EMAIL_FIELD_NUMBER: _ClassVar[int] @@ -45,11 +45,11 @@ class AliasDeleted(_message.Message): alias_email: str def __init__(self, alias_id: _Optional[int] = ..., alias_email: _Optional[str] = ...) -> None: ... -class AliasCreateList(_message.Message): - __slots__ = ("event",) - EVENT_FIELD_NUMBER: _ClassVar[int] - event: _containers.RepeatedCompositeFieldContainer[AliasCreated] - def __init__(self, event: _Optional[_Iterable[_Union[AliasCreated, _Mapping]]] = ...) -> None: ... +class AliasCreatedList(_message.Message): + __slots__ = ("events",) + EVENTS_FIELD_NUMBER: _ClassVar[int] + events: _containers.RepeatedCompositeFieldContainer[AliasCreated] + def __init__(self, events: _Optional[_Iterable[_Union[AliasCreated, _Mapping]]] = ...) -> None: ... class EventContent(_message.Message): __slots__ = ("user_plan_change", "user_deleted", "alias_created", "alias_status_change", "alias_deleted", "alias_create_list") @@ -59,13 +59,13 @@ class EventContent(_message.Message): ALIAS_STATUS_CHANGE_FIELD_NUMBER: _ClassVar[int] ALIAS_DELETED_FIELD_NUMBER: _ClassVar[int] ALIAS_CREATE_LIST_FIELD_NUMBER: _ClassVar[int] - user_plan_change: UserPlanChange + user_plan_change: UserPlanChanged user_deleted: UserDeleted alias_created: AliasCreated - alias_status_change: AliasStatusChange + alias_status_change: AliasStatusChanged alias_deleted: AliasDeleted - alias_create_list: AliasCreateList - def __init__(self, user_plan_change: _Optional[_Union[UserPlanChange, _Mapping]] = ..., user_deleted: _Optional[_Union[UserDeleted, _Mapping]] = ..., alias_created: _Optional[_Union[AliasCreated, _Mapping]] = ..., alias_status_change: _Optional[_Union[AliasStatusChange, _Mapping]] = ..., alias_deleted: _Optional[_Union[AliasDeleted, _Mapping]] = ..., alias_create_list: _Optional[_Union[AliasCreateList, _Mapping]] = ...) -> None: ... + alias_create_list: AliasCreatedList + def __init__(self, user_plan_change: _Optional[_Union[UserPlanChanged, _Mapping]] = ..., user_deleted: _Optional[_Union[UserDeleted, _Mapping]] = ..., alias_created: _Optional[_Union[AliasCreated, _Mapping]] = ..., alias_status_change: _Optional[_Union[AliasStatusChanged, _Mapping]] = ..., alias_deleted: _Optional[_Union[AliasDeleted, _Mapping]] = ..., alias_create_list: _Optional[_Union[AliasCreatedList, _Mapping]] = ...) -> None: ... class Event(_message.Message): __slots__ = ("user_id", "external_user_id", "partner_id", "content") diff --git a/app/handler/unsubscribe_handler.py b/app/handler/unsubscribe_handler.py index 88e4831d..21faeb58 100644 --- a/app/handler/unsubscribe_handler.py +++ b/app/handler/unsubscribe_handler.py @@ -102,6 +102,7 @@ class UnsubscribeHandler: mailbox.email, alias ): return status.E509 + LOG.i(f"User disabled alias {alias} via unsubscribe header") alias_utils.change_alias_status(alias, enabled=False) Session.commit() enable_alias_url = config.URL + f"/dashboard/?highlight_alias_id={alias.id}" diff --git a/app/jobs/event_jobs.py b/app/jobs/event_jobs.py new file mode 100644 index 00000000..e019d348 --- /dev/null +++ b/app/jobs/event_jobs.py @@ -0,0 +1,40 @@ +from app.events.event_dispatcher import EventDispatcher, Dispatcher +from app.events.generated.event_pb2 import EventContent, AliasCreated, AliasCreatedList +from app.log import LOG +from app.models import User, Alias + + +def send_alias_creation_events_for_user( + user: User, dispatcher: Dispatcher, chunk_size=50 +): + if user.disabled: + LOG.i("User {user} is disabled. Skipping sending events for that user") + return + chunk_size = min(chunk_size, 50) + event_list = [] + for alias in ( + Alias.yield_per_query(chunk_size) + .filter_by(user_id=user.id) + .order_by(Alias.id.asc()) + ): + event_list.append( + AliasCreated( + alias_id=alias.id, + alias_email=alias.email, + alias_note=alias.note, + enabled=alias.enabled, + ) + ) + if len(event_list) >= chunk_size: + EventDispatcher.send_event( + user, + EventContent(alias_create_list=AliasCreatedList(events=event_list)), + dispatcher=dispatcher, + ) + event_list = [] + if len(event_list) > 0: + EventDispatcher.send_event( + user, + EventContent(alias_create_list=AliasCreatedList(events=event_list)), + dispatcher=dispatcher, + ) diff --git a/app/subscription_webhook.py b/app/subscription_webhook.py index 4b4ce075..e117153c 100644 --- a/app/subscription_webhook.py +++ b/app/subscription_webhook.py @@ -3,7 +3,7 @@ from requests import RequestException from app import config from app.events.event_dispatcher import EventDispatcher -from app.events.generated.event_pb2 import EventContent, UserPlanChange +from app.events.generated.event_pb2 import EventContent, UserPlanChanged from app.log import LOG from app.models import User @@ -34,5 +34,5 @@ def execute_subscription_webhook(user: User): except RequestException as e: LOG.error(f"Subscription request exception: {e}") - event = UserPlanChange(plan_end_time=sl_subscription_end) + event = UserPlanChanged(plan_end_time=sl_subscription_end) EventDispatcher.send_event(user, EventContent(user_plan_change=event)) diff --git a/job_runner.py b/job_runner.py index 590611db..6674964a 100644 --- a/job_runner.py +++ b/job_runner.py @@ -15,6 +15,7 @@ from app.email_utils import ( render, ) from app.import_utils import handle_batch_import +from app.jobs.event_jobs import send_alias_creation_events_for_user from app.jobs.export_user_data_job import ExportUserDataJob from app.log import LOG from app.models import User, Job, BatchImport, Mailbox, CustomDomain, JobState @@ -264,8 +265,14 @@ SimpleLogin team. user_id = job.payload.get("user_id") user = User.get(user_id) if user and user.activated: - LOG.d("send proton welcome email to user %s", user) + LOG.d("Send proton welcome email to user %s", user) welcome_proton(user) + elif job.name == config.JOB_SEND_ALIAS_CREATION_EVENTS: + user_id = job.payload.get("user_id") + user = User.get(user_id) + if user and user.activated: + LOG.d(f"Sending alias creation events for {user}") + send_alias_creation_events_for_user(user) else: LOG.e("Unknown job name %s", job.name) diff --git a/poetry.lock b/poetry.lock index 49423284..9c34dd0b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2150,24 +2150,22 @@ wcwidth = "*" [[package]] name = "protobuf" -version = "4.24.3" +version = "5.27.1" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "protobuf-4.24.3-cp310-abi3-win32.whl", hash = "sha256:20651f11b6adc70c0f29efbe8f4a94a74caf61b6200472a9aea6e19898f9fcf4"}, - {file = "protobuf-4.24.3-cp310-abi3-win_amd64.whl", hash = "sha256:3d42e9e4796a811478c783ef63dc85b5a104b44aaaca85d4864d5b886e4b05e3"}, - {file = "protobuf-4.24.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e514e8af0045be2b56e56ae1bb14f43ce7ffa0f68b1c793670ccbe2c4fc7d2b"}, - {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ba53c2f04798a326774f0e53b9c759eaef4f6a568ea7072ec6629851c8435959"}, - {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f6ccbcf027761a2978c1406070c3788f6de4a4b2cc20800cc03d52df716ad675"}, - {file = "protobuf-4.24.3-cp37-cp37m-win32.whl", hash = "sha256:1b182c7181a2891e8f7f3a1b5242e4ec54d1f42582485a896e4de81aa17540c2"}, - {file = "protobuf-4.24.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b0271a701e6782880d65a308ba42bc43874dabd1a0a0f41f72d2dac3b57f8e76"}, - {file = "protobuf-4.24.3-cp38-cp38-win32.whl", hash = "sha256:e29d79c913f17a60cf17c626f1041e5288e9885c8579832580209de8b75f2a52"}, - {file = "protobuf-4.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:067f750169bc644da2e1ef18c785e85071b7c296f14ac53e0900e605da588719"}, - {file = "protobuf-4.24.3-cp39-cp39-win32.whl", hash = "sha256:2da777d34b4f4f7613cdf85c70eb9a90b1fbef9d36ae4a0ccfe014b0b07906f1"}, - {file = "protobuf-4.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:f631bb982c5478e0c1c70eab383af74a84be66945ebf5dd6b06fc90079668d0b"}, - {file = "protobuf-4.24.3-py3-none-any.whl", hash = "sha256:f6f8dc65625dadaad0c8545319c2e2f0424fede988368893ca3844261342c11a"}, - {file = "protobuf-4.24.3.tar.gz", hash = "sha256:12e9ad2ec079b833176d2921be2cb24281fa591f0b119b208b788adc48c2561d"}, + {file = "protobuf-5.27.1-cp310-abi3-win32.whl", hash = "sha256:3adc15ec0ff35c5b2d0992f9345b04a540c1e73bfee3ff1643db43cc1d734333"}, + {file = "protobuf-5.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:25236b69ab4ce1bec413fd4b68a15ef8141794427e0b4dc173e9d5d9dffc3bcd"}, + {file = "protobuf-5.27.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4e38fc29d7df32e01a41cf118b5a968b1efd46b9c41ff515234e794011c78b17"}, + {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:917ed03c3eb8a2d51c3496359f5b53b4e4b7e40edfbdd3d3f34336e0eef6825a"}, + {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:ee52874a9e69a30271649be88ecbe69d374232e8fd0b4e4b0aaaa87f429f1631"}, + {file = "protobuf-5.27.1-cp38-cp38-win32.whl", hash = "sha256:7a97b9c5aed86b9ca289eb5148df6c208ab5bb6906930590961e08f097258107"}, + {file = "protobuf-5.27.1-cp38-cp38-win_amd64.whl", hash = "sha256:f6abd0f69968792da7460d3c2cfa7d94fd74e1c21df321eb6345b963f9ec3d8d"}, + {file = "protobuf-5.27.1-cp39-cp39-win32.whl", hash = "sha256:dfddb7537f789002cc4eb00752c92e67885badcc7005566f2c5de9d969d3282d"}, + {file = "protobuf-5.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:39309898b912ca6febb0084ea912e976482834f401be35840a008da12d189340"}, + {file = "protobuf-5.27.1-py3-none-any.whl", hash = "sha256:4ac7249a1530a2ed50e24201d6630125ced04b30619262f06224616e0030b6cf"}, + {file = "protobuf-5.27.1.tar.gz", hash = "sha256:df5e5b8e39b7d1c25b186ffdf9f44f40f810bbcc9d2b71d9d3156fee5a9adf15"}, ] [[package]] diff --git a/proto/event.proto b/proto/event.proto index 8c7a5c7e..694849e1 100644 --- a/proto/event.proto +++ b/proto/event.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package simplelogin_events; -message UserPlanChange { +message UserPlanChanged { uint32 plan_end_time = 1; } @@ -16,7 +16,7 @@ message AliasCreated { bool enabled = 4; } -message AliasStatusChange { +message AliasStatusChanged { uint32 alias_id = 1; string alias_email = 2; bool enabled = 3; @@ -27,18 +27,18 @@ message AliasDeleted { string alias_email = 2; } -message AliasCreateList { - repeated AliasCreated event = 1; +message AliasCreatedList { + repeated AliasCreated events = 1; } message EventContent { oneof content { - UserPlanChange user_plan_change = 1; + UserPlanChanged user_plan_change = 1; UserDeleted user_deleted = 2; AliasCreated alias_created = 3; - AliasStatusChange alias_status_change = 4; + AliasStatusChanged alias_status_change = 4; AliasDeleted alias_deleted = 5; - AliasCreateList alias_create_list = 6; + AliasCreatedList alias_create_list = 6; } } diff --git a/tests/jobs/test_send_alias_creation_events.py b/tests/jobs/test_send_alias_creation_events.py new file mode 100644 index 00000000..de53ac1c --- /dev/null +++ b/tests/jobs/test_send_alias_creation_events.py @@ -0,0 +1,46 @@ +from app import config +from app.db import Session +from app.events.event_dispatcher import Dispatcher +from app.events.generated import event_pb2 +from app.jobs.event_jobs import send_alias_creation_events_for_user +from app.models import Alias +from tests.utils import create_partner_linked_user + + +class MemStoreDispatcher(Dispatcher): + def __init__(self): + self.events = [] + + def send(self, event: bytes): + self.events.append(event) + + +def setup_module(): + config.EVENT_WEBHOOK = True + + +def teardown_module(): + config.EVENT_WEBHOOK = False + + +def test_send_alias_creation_events(): + [user, partner_user] = create_partner_linked_user() + aliases = [Alias.create_new_random(user) for i in range(2)] + Session.flush() + dispatcher = MemStoreDispatcher() + send_alias_creation_events_for_user(user, dispatcher=dispatcher, chunk_size=2) + # 2 batches. 1st newsletter + first alias. 2nd last alias + assert len(dispatcher.events) == 2 + decoded_event = event_pb2.Event.FromString(dispatcher.events[0]) + assert decoded_event.user_id == user.id + assert decoded_event.external_user_id == partner_user.external_user_id + event_list = decoded_event.content.alias_create_list.events + assert len(event_list) == 2 + # 0 is newsletter alias + assert event_list[1].alias_id == aliases[0].id + decoded_event = event_pb2.Event.FromString(dispatcher.events[1]) + assert decoded_event.user_id == user.id + assert decoded_event.external_user_id == partner_user.external_user_id + event_list = decoded_event.content.alias_create_list.events + assert len(event_list) == 1 + assert event_list[0].alias_id == aliases[1].id diff --git a/tests/utils.py b/tests/utils.py index fe4f5654..179cbb2e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,7 +9,8 @@ from typing import Optional, Dict import jinja2 from flask import url_for -from app.models import User +from app.models import User, PartnerUser +from app.proton.utils import get_proton_partner from app.utils import random_string @@ -30,6 +31,18 @@ def create_new_user(email: Optional[str] = None, name: Optional[str] = None) -> return user +def create_partner_linked_user() -> tuple[User, PartnerUser]: + user = create_new_user() + partner_user = PartnerUser.create( + partner_id=get_proton_partner().id, + user_id=user.id, + external_user_id=random_token(10), + flush=True, + ) + + return user, partner_user + + def login(flask_client, user: Optional[User] = None) -> User: if not user: user = create_new_user()