snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
766
zerver/lib/event_schema.py
Normal file
766
zerver/lib/event_schema.py
Normal file
@@ -0,0 +1,766 @@
|
||||
# This module is a collection of testing helpers for validating the
|
||||
# schema of "events" sent by Zulip's server-to-client push system.
|
||||
#
|
||||
# By policy, every event generated by Zulip's API should be validated
|
||||
# by a test in test_events.py with a schema checker here.
|
||||
#
|
||||
# See https://zulip.readthedocs.io/en/latest/subsystems/events-system.html
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from pprint import PrettyPrinter
|
||||
from typing import cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zerver.lib.event_types import (
|
||||
AlertWordsEvent,
|
||||
AllowMessageEditingData,
|
||||
AttachmentAddEvent,
|
||||
AttachmentRemoveEvent,
|
||||
AttachmentUpdateEvent,
|
||||
AuthenticationData,
|
||||
BaseEvent,
|
||||
BotServicesEmbedded,
|
||||
BotServicesOutgoing,
|
||||
ChannelFolderAddEvent,
|
||||
ChannelFolderReorderEvent,
|
||||
ChannelFolderUpdateEvent,
|
||||
CustomProfileFieldsEvent,
|
||||
DefaultStreamGroupsEvent,
|
||||
DefaultStreamsEvent,
|
||||
DeleteMessageEvent,
|
||||
DeviceAddEvent,
|
||||
DeviceRemoveEvent,
|
||||
DeviceUpdateEvent,
|
||||
DirectMessageEvent,
|
||||
DraftsAddEvent,
|
||||
DraftsRemoveEvent,
|
||||
DraftsUpdateEvent,
|
||||
GroupSettingUpdateData,
|
||||
HasWebexTokenEvent,
|
||||
HasZoomTokenEvent,
|
||||
HeartbeatEvent,
|
||||
IconData,
|
||||
InvitesChangedEvent,
|
||||
LegacyPresenceEvent,
|
||||
LogoData,
|
||||
MessageContentEditLimitSecondsData,
|
||||
MessageEvent,
|
||||
ModernPresenceEvent,
|
||||
MutedTopicsEvent,
|
||||
MutedUsersEvent,
|
||||
NavigationViewAddEvent,
|
||||
NavigationViewRemoveEvent,
|
||||
NavigationViewUpdateEvent,
|
||||
NightLogoData,
|
||||
OnboardingStepsEvent,
|
||||
PersonAvatarFields,
|
||||
PersonBotOwnerId,
|
||||
PersonCustomProfileField,
|
||||
PersonDateJoined,
|
||||
PersonDeliveryEmail,
|
||||
PersonEmail,
|
||||
PersonFullName,
|
||||
PersonIsActive,
|
||||
PersonIsImportedStub,
|
||||
PersonRole,
|
||||
PersonTimezone,
|
||||
PlanTypeData,
|
||||
ReactionAddEvent,
|
||||
ReactionRemoveEvent,
|
||||
RealmBotAddEvent,
|
||||
RealmBotDeleteEvent,
|
||||
RealmBotUpdateEvent,
|
||||
RealmDeactivatedEvent,
|
||||
RealmDescriptionData,
|
||||
RealmDomainsAddEvent,
|
||||
RealmDomainsChangeEvent,
|
||||
RealmDomainsRemoveEvent,
|
||||
RealmEmojiAddEvent,
|
||||
RealmEmojiUpdateEvent,
|
||||
RealmEmojiUpdateOneEvent,
|
||||
RealmExportConsentEvent,
|
||||
RealmExportEvent,
|
||||
RealmLinkifiersEvent,
|
||||
RealmPlaygroundsEvent,
|
||||
RealmTopicsPolicyData,
|
||||
RealmUpdateDictEvent,
|
||||
RealmUpdateEvent,
|
||||
RealmUserAddEvent,
|
||||
RealmUserRemoveEvent,
|
||||
RealmUserSettingsDefaultsUpdateEvent,
|
||||
RealmUserUpdateEvent,
|
||||
RemindersAddEvent,
|
||||
RemindersRemoveEvent,
|
||||
RestartEvent,
|
||||
SavedSnippetsAddEvent,
|
||||
SavedSnippetsRemoveEvent,
|
||||
SavedSnippetsUpdateEvent,
|
||||
ScheduledMessagesAddEvent,
|
||||
ScheduledMessagesRemoveEvent,
|
||||
ScheduledMessagesUpdateEvent,
|
||||
StreamCreateEvent,
|
||||
StreamDeleteEvent,
|
||||
StreamUpdateEvent,
|
||||
SubmessageEvent,
|
||||
SubscriptionAddEvent,
|
||||
SubscriptionPeerAddEvent,
|
||||
SubscriptionPeerRemoveEvent,
|
||||
SubscriptionRemoveEvent,
|
||||
SubscriptionUpdateEvent,
|
||||
TypingEditMessageStartEvent,
|
||||
TypingEditMessageStopEvent,
|
||||
TypingStartEvent,
|
||||
TypingStopEvent,
|
||||
UpdateMessageEvent,
|
||||
UpdateMessageFlagsAddEvent,
|
||||
UpdateMessageFlagsRemoveEvent,
|
||||
UserGroupAddEvent,
|
||||
UserGroupAddMembersEvent,
|
||||
UserGroupAddSubgroupsEvent,
|
||||
UserGroupRemoveEvent,
|
||||
UserGroupRemoveMembersEvent,
|
||||
UserGroupRemoveSubgroupsEvent,
|
||||
UserGroupUpdateEvent,
|
||||
UserSettingsUpdateEvent,
|
||||
UserStatusEvent,
|
||||
UserTopicEvent,
|
||||
WebReloadClientEvent,
|
||||
)
|
||||
from zerver.lib.topic import ORIG_TOPIC, TOPIC_NAME
|
||||
from zerver.lib.types import UserGroupMembersDict
|
||||
from zerver.models import Realm, RealmUserDefault, Stream, UserProfile
|
||||
from zerver.models.streams import StreamTopicsPolicyEnum
|
||||
|
||||
|
||||
def validate_with_model(data: dict[str, object], model: type[BaseModel]) -> None:
|
||||
allowed_fields = set(model.model_fields.keys())
|
||||
if not set(data.keys()).issubset(allowed_fields): # nocoverage
|
||||
raise ValueError(f"Extra fields not allowed: {set(data.keys()) - allowed_fields}")
|
||||
|
||||
model.model_validate(data, strict=True)
|
||||
|
||||
|
||||
def make_checker(base_model: type[BaseEvent]) -> Callable[[str, dict[str, object]], None]:
|
||||
def f(label: str, event: dict[str, object]) -> None:
|
||||
assert isinstance(event["id"], int)
|
||||
event = dict(event)
|
||||
del event["id"]
|
||||
try:
|
||||
validate_with_model(event, base_model)
|
||||
except Exception: # nocoverage
|
||||
print(f"""
|
||||
FAILURE:
|
||||
|
||||
The event below fails the check to make sure it has the
|
||||
correct "shape" of data:
|
||||
|
||||
{label}
|
||||
|
||||
Often this is a symptom that the following type definition
|
||||
is either broken or needs to be updated due to other
|
||||
changes that you have made:
|
||||
|
||||
{base_model}
|
||||
|
||||
A traceback should follow to help you debug this problem.
|
||||
|
||||
Here is the event:
|
||||
""")
|
||||
|
||||
PrettyPrinter(indent=4).pprint(event)
|
||||
raise
|
||||
|
||||
return f
|
||||
|
||||
|
||||
check_alert_words = make_checker(AlertWordsEvent)
|
||||
check_attachment_add = make_checker(AttachmentAddEvent)
|
||||
check_attachment_remove = make_checker(AttachmentRemoveEvent)
|
||||
check_attachment_update = make_checker(AttachmentUpdateEvent)
|
||||
check_channel_folder_add = make_checker(ChannelFolderAddEvent)
|
||||
check_channel_folder_reorder = make_checker(ChannelFolderReorderEvent)
|
||||
check_custom_profile_fields = make_checker(CustomProfileFieldsEvent)
|
||||
check_default_stream_groups = make_checker(DefaultStreamGroupsEvent)
|
||||
check_default_streams = make_checker(DefaultStreamsEvent)
|
||||
check_device_add = make_checker(DeviceAddEvent)
|
||||
check_device_remove = make_checker(DeviceRemoveEvent)
|
||||
check_device_update = make_checker(DeviceUpdateEvent)
|
||||
check_direct_message = make_checker(DirectMessageEvent)
|
||||
check_draft_add = make_checker(DraftsAddEvent)
|
||||
check_draft_remove = make_checker(DraftsRemoveEvent)
|
||||
check_draft_update = make_checker(DraftsUpdateEvent)
|
||||
check_heartbeat = make_checker(HeartbeatEvent)
|
||||
check_invites_changed = make_checker(InvitesChangedEvent)
|
||||
check_message = make_checker(MessageEvent)
|
||||
check_muted_users = make_checker(MutedUsersEvent)
|
||||
check_navigation_view_add = make_checker(NavigationViewAddEvent)
|
||||
check_navigation_view_remove = make_checker(NavigationViewRemoveEvent)
|
||||
check_navigation_view_update = make_checker(NavigationViewUpdateEvent)
|
||||
check_onboarding_steps = make_checker(OnboardingStepsEvent)
|
||||
check_reaction_add = make_checker(ReactionAddEvent)
|
||||
check_reaction_remove = make_checker(ReactionRemoveEvent)
|
||||
check_realm_bot_delete = make_checker(RealmBotDeleteEvent)
|
||||
check_realm_deactivated = make_checker(RealmDeactivatedEvent)
|
||||
check_realm_domains_add = make_checker(RealmDomainsAddEvent)
|
||||
check_realm_domains_change = make_checker(RealmDomainsChangeEvent)
|
||||
check_realm_domains_remove = make_checker(RealmDomainsRemoveEvent)
|
||||
check_realm_emoji_add = make_checker(RealmEmojiAddEvent)
|
||||
check_realm_emoji_update_one = make_checker(RealmEmojiUpdateOneEvent)
|
||||
check_realm_export_consent = make_checker(RealmExportConsentEvent)
|
||||
check_realm_linkifiers = make_checker(RealmLinkifiersEvent)
|
||||
check_realm_playgrounds = make_checker(RealmPlaygroundsEvent)
|
||||
check_realm_user_add = make_checker(RealmUserAddEvent)
|
||||
check_realm_user_remove = make_checker(RealmUserRemoveEvent)
|
||||
check_reminder_add = make_checker(RemindersAddEvent)
|
||||
check_reminder_remove = make_checker(RemindersRemoveEvent)
|
||||
check_restart = make_checker(RestartEvent)
|
||||
check_saved_snippets_add = make_checker(SavedSnippetsAddEvent)
|
||||
check_saved_snippets_remove = make_checker(SavedSnippetsRemoveEvent)
|
||||
check_saved_snippets_update = make_checker(SavedSnippetsUpdateEvent)
|
||||
check_scheduled_message_add = make_checker(ScheduledMessagesAddEvent)
|
||||
check_scheduled_message_remove = make_checker(ScheduledMessagesRemoveEvent)
|
||||
check_scheduled_message_update = make_checker(ScheduledMessagesUpdateEvent)
|
||||
check_stream_create = make_checker(StreamCreateEvent)
|
||||
check_stream_delete = make_checker(StreamDeleteEvent)
|
||||
check_submessage = make_checker(SubmessageEvent)
|
||||
check_subscription_add = make_checker(SubscriptionAddEvent)
|
||||
check_subscription_peer_add = make_checker(SubscriptionPeerAddEvent)
|
||||
check_subscription_peer_remove = make_checker(SubscriptionPeerRemoveEvent)
|
||||
check_subscription_remove = make_checker(SubscriptionRemoveEvent)
|
||||
check_typing_start = make_checker(TypingStartEvent)
|
||||
check_typing_stop = make_checker(TypingStopEvent)
|
||||
check_typing_edit_message_start = make_checker(TypingEditMessageStartEvent)
|
||||
check_typing_edit_message_stop = make_checker(TypingEditMessageStopEvent)
|
||||
check_update_message_flags_add = make_checker(UpdateMessageFlagsAddEvent)
|
||||
check_update_message_flags_remove = make_checker(UpdateMessageFlagsRemoveEvent)
|
||||
check_user_group_add = make_checker(UserGroupAddEvent)
|
||||
check_user_group_add_members = make_checker(UserGroupAddMembersEvent)
|
||||
check_user_group_add_subgroups = make_checker(UserGroupAddSubgroupsEvent)
|
||||
check_user_group_remove = make_checker(UserGroupRemoveEvent)
|
||||
check_user_group_remove_members = make_checker(UserGroupRemoveMembersEvent)
|
||||
check_user_group_remove_subgroups = make_checker(UserGroupRemoveSubgroupsEvent)
|
||||
check_user_topic = make_checker(UserTopicEvent)
|
||||
check_web_reload_client_event = make_checker(WebReloadClientEvent)
|
||||
|
||||
|
||||
# Now for the slightly more tricky bits. All the following functions
|
||||
# get wrapped with more stringent checkers. Some of the wrappers are
|
||||
# reasonably sane functions that just check the data, not the shape of
|
||||
# the data.
|
||||
#
|
||||
# But there are some ugly wrappers that work around the fact that
|
||||
# a) our events are not very consistent and/or b) our types aren't
|
||||
# robust.
|
||||
#
|
||||
# TODO: work through the bottom of this file to try to find ways to
|
||||
# simplify our types or make them more robust
|
||||
|
||||
_check_channel_folder_update = make_checker(ChannelFolderUpdateEvent)
|
||||
_check_delete_message = make_checker(DeleteMessageEvent)
|
||||
_check_has_zoom_token = make_checker(HasZoomTokenEvent)
|
||||
_check_has_webex_token = make_checker(HasWebexTokenEvent)
|
||||
_check_legacy_presence = make_checker(LegacyPresenceEvent)
|
||||
_check_modern_presence = make_checker(ModernPresenceEvent)
|
||||
_check_muted_topics = make_checker(MutedTopicsEvent)
|
||||
_check_realm_bot_add = make_checker(RealmBotAddEvent)
|
||||
_check_realm_bot_update = make_checker(RealmBotUpdateEvent)
|
||||
_check_realm_default_update = make_checker(RealmUserSettingsDefaultsUpdateEvent)
|
||||
_check_realm_emoji_update = make_checker(RealmEmojiUpdateEvent)
|
||||
_check_realm_export = make_checker(RealmExportEvent)
|
||||
_check_realm_update = make_checker(RealmUpdateEvent)
|
||||
_check_realm_update_dict = make_checker(RealmUpdateDictEvent)
|
||||
_check_realm_user_update = make_checker(RealmUserUpdateEvent)
|
||||
_check_stream_update = make_checker(StreamUpdateEvent)
|
||||
_check_subscription_update = make_checker(SubscriptionUpdateEvent)
|
||||
_check_update_message = make_checker(UpdateMessageEvent)
|
||||
_check_user_group_update = make_checker(UserGroupUpdateEvent)
|
||||
_check_user_settings_update = make_checker(UserSettingsUpdateEvent)
|
||||
_check_user_status = make_checker(UserStatusEvent)
|
||||
|
||||
|
||||
PERSON_TYPES: dict[str, type[BaseModel]] = dict(
|
||||
avatar_fields=PersonAvatarFields,
|
||||
bot_owner_id=PersonBotOwnerId,
|
||||
custom_profile_field=PersonCustomProfileField,
|
||||
date_joined=PersonDateJoined,
|
||||
delivery_email=PersonDeliveryEmail,
|
||||
email=PersonEmail,
|
||||
full_name=PersonFullName,
|
||||
role=PersonRole,
|
||||
timezone=PersonTimezone,
|
||||
is_active=PersonIsActive,
|
||||
is_imported_stub=PersonIsImportedStub,
|
||||
)
|
||||
|
||||
|
||||
def check_channel_folder_update(var_name: str, event: dict[str, object], fields: set[str]) -> None:
|
||||
_check_channel_folder_update(var_name, event)
|
||||
|
||||
assert isinstance(event["data"], dict)
|
||||
assert set(event["data"].keys()) == fields
|
||||
|
||||
|
||||
def check_delete_message(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
message_type: str,
|
||||
num_message_ids: int,
|
||||
is_legacy: bool,
|
||||
) -> None:
|
||||
_check_delete_message(var_name, event)
|
||||
|
||||
keys = {"id", "type", "message_type"}
|
||||
|
||||
assert event["message_type"] == message_type
|
||||
|
||||
if message_type == "stream":
|
||||
keys |= {"stream_id", "topic"}
|
||||
elif message_type == "private":
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("unexpected message_type")
|
||||
|
||||
if is_legacy:
|
||||
assert num_message_ids == 1
|
||||
keys.add("message_id")
|
||||
else:
|
||||
assert isinstance(event["message_ids"], list)
|
||||
assert num_message_ids == len(event["message_ids"])
|
||||
keys.add("message_ids")
|
||||
|
||||
assert set(event.keys()) == keys
|
||||
|
||||
|
||||
def check_has_zoom_token(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
value: bool,
|
||||
) -> None:
|
||||
_check_has_zoom_token(var_name, event)
|
||||
assert event["value"] == value
|
||||
|
||||
|
||||
def check_has_webex_token(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
value: bool,
|
||||
) -> None:
|
||||
_check_has_webex_token(var_name, event)
|
||||
assert event["value"] == value
|
||||
|
||||
|
||||
def check_muted_topics(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
) -> None:
|
||||
_check_muted_topics(var_name, event)
|
||||
muted_topics = event["muted_topics"]
|
||||
assert isinstance(muted_topics, list)
|
||||
for muted_topic in muted_topics:
|
||||
muted_topic_tuple = tuple(muted_topic)
|
||||
assert list(map(type, muted_topic_tuple)) == [str, str, int]
|
||||
|
||||
|
||||
def check_legacy_presence(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
has_email: bool,
|
||||
presence_key: str,
|
||||
status: str,
|
||||
) -> None:
|
||||
_check_legacy_presence(var_name, event)
|
||||
|
||||
assert ("email" in event) == has_email
|
||||
|
||||
assert isinstance(event["presence"], dict)
|
||||
|
||||
# Our tests only have one presence value.
|
||||
[(event_presence_key, event_presence_value)] = event["presence"].items()
|
||||
assert event_presence_key == presence_key
|
||||
assert event_presence_value["status"] == status
|
||||
|
||||
|
||||
def check_modern_presence(var_name: str, event: dict[str, object], user_id: int) -> None:
|
||||
_check_modern_presence(var_name, event)
|
||||
|
||||
assert isinstance(event["presences"], dict)
|
||||
|
||||
[(event_presences_key, _event_presences_value)] = event["presences"].items()
|
||||
assert event_presences_key == str(user_id)
|
||||
|
||||
|
||||
def check_realm_bot_add(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
bot_type: int,
|
||||
) -> None:
|
||||
_check_realm_bot_add(var_name, event)
|
||||
|
||||
assert isinstance(event["bot"], dict)
|
||||
|
||||
services = event["bot"]["services"]
|
||||
|
||||
if bot_type == UserProfile.DEFAULT_BOT:
|
||||
assert services == []
|
||||
elif bot_type == UserProfile.OUTGOING_WEBHOOK_BOT:
|
||||
assert len(services) == 1
|
||||
validate_with_model(services[0], BotServicesOutgoing)
|
||||
elif bot_type == UserProfile.EMBEDDED_BOT:
|
||||
assert len(services) == 1
|
||||
validate_with_model(services[0], BotServicesEmbedded)
|
||||
else:
|
||||
raise AssertionError(f"Unknown bot_type: {bot_type}")
|
||||
|
||||
|
||||
def check_realm_bot_update(
|
||||
# Check schema plus the field.
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
field: str,
|
||||
) -> None:
|
||||
# Check the overall schema first.
|
||||
_check_realm_bot_update(var_name, event)
|
||||
|
||||
assert isinstance(event["bot"], dict)
|
||||
assert {"user_id", field} == set(event["bot"].keys())
|
||||
|
||||
|
||||
def check_realm_emoji_update(var_name: str, event: dict[str, object]) -> None:
|
||||
"""
|
||||
The way we send realm emojis is kinda clumsy--we
|
||||
send a dict mapping the emoji id to a sub_dict with
|
||||
the fields (including the id). Ideally we can streamline
|
||||
this and just send a list of dicts. The clients can make
|
||||
a Map as needed.
|
||||
"""
|
||||
_check_realm_emoji_update(var_name, event)
|
||||
|
||||
assert isinstance(event["realm_emoji"], dict)
|
||||
for k, v in event["realm_emoji"].items():
|
||||
assert v["id"] == k
|
||||
|
||||
|
||||
def check_realm_export(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
has_export_url: bool,
|
||||
has_deleted_timestamp: bool,
|
||||
has_failed_timestamp: bool,
|
||||
) -> None:
|
||||
# Check the overall event first, knowing it has some
|
||||
# optional types.
|
||||
_check_realm_export(var_name, event)
|
||||
|
||||
# It's possible to have multiple data exports, but the events tests do not
|
||||
# exercise that case, so we do strict validation for a single export here.
|
||||
assert isinstance(event["exports"], list)
|
||||
assert len(event["exports"]) == 1
|
||||
export = event["exports"][0]
|
||||
|
||||
# Now verify which fields have non-None values.
|
||||
assert has_export_url == (export["export_url"] is not None)
|
||||
assert has_deleted_timestamp == (export["deleted_timestamp"] is not None)
|
||||
assert has_failed_timestamp == (export["failed_timestamp"] is not None)
|
||||
|
||||
|
||||
def check_realm_update(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
prop: str,
|
||||
) -> None:
|
||||
"""
|
||||
Realm updates have these two fields:
|
||||
|
||||
property
|
||||
value
|
||||
|
||||
We check not only the basic schema, but also that
|
||||
the value people actually matches the type from
|
||||
Realm.property_types that we have configured
|
||||
for the property.
|
||||
|
||||
For certain properties, there are extra fields.
|
||||
For example, when property is "description", there's also
|
||||
"rendered_description".
|
||||
"""
|
||||
_check_realm_update(var_name, event)
|
||||
|
||||
assert prop == event["property"]
|
||||
value = event["value"]
|
||||
|
||||
if prop in [
|
||||
"moderation_request_channel_id",
|
||||
"new_stream_announcements_stream_id",
|
||||
"signup_announcements_stream_id",
|
||||
"zulip_update_announcements_stream_id",
|
||||
"org_type",
|
||||
]:
|
||||
assert isinstance(value, int)
|
||||
return
|
||||
|
||||
property_type = Realm.property_types[prop]
|
||||
if inspect.isclass(property_type) and issubclass(property_type, Enum):
|
||||
assert isinstance(value, str)
|
||||
property_type[value]
|
||||
else:
|
||||
assert isinstance(value, property_type)
|
||||
|
||||
|
||||
def check_realm_default_update(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
prop: str,
|
||||
) -> None:
|
||||
_check_realm_default_update(var_name, event)
|
||||
|
||||
assert prop == event["property"]
|
||||
assert prop != "default_language"
|
||||
assert prop in RealmUserDefault.property_types
|
||||
|
||||
prop_type = RealmUserDefault.property_types[prop]
|
||||
value = event["value"]
|
||||
if inspect.isclass(prop_type) and issubclass(prop_type, Enum):
|
||||
assert isinstance(value, str)
|
||||
prop_type[value]
|
||||
else:
|
||||
assert isinstance(value, prop_type)
|
||||
|
||||
|
||||
def check_realm_update_dict(
|
||||
# handle union types
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
) -> None:
|
||||
_check_realm_update_dict(var_name, event)
|
||||
|
||||
if event["property"] == "default":
|
||||
assert isinstance(event["data"], dict)
|
||||
|
||||
if "allow_message_editing" in event["data"]:
|
||||
sub_type: type[BaseModel] = AllowMessageEditingData
|
||||
elif "message_content_edit_limit_seconds" in event["data"]:
|
||||
sub_type = MessageContentEditLimitSecondsData
|
||||
elif "authentication_methods" in event["data"]:
|
||||
sub_type = AuthenticationData
|
||||
elif any(
|
||||
setting_name in event["data"] for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS
|
||||
):
|
||||
sub_type = GroupSettingUpdateData
|
||||
elif "plan_type" in event["data"]:
|
||||
sub_type = PlanTypeData
|
||||
elif "topics_policy" in event["data"]:
|
||||
sub_type = RealmTopicsPolicyData
|
||||
elif "description" in event["data"]:
|
||||
sub_type = RealmDescriptionData
|
||||
else:
|
||||
raise AssertionError("unhandled fields in data")
|
||||
|
||||
elif event["property"] == "icon":
|
||||
sub_type = IconData
|
||||
elif event["property"] == "logo":
|
||||
sub_type = LogoData
|
||||
elif event["property"] == "night_logo":
|
||||
sub_type = NightLogoData
|
||||
else:
|
||||
raise AssertionError("unhandled property: {event['property']}")
|
||||
|
||||
validate_with_model(cast(dict[str, object], event["data"]), sub_type)
|
||||
|
||||
|
||||
def check_realm_user_update(
|
||||
# person_flavor tells us which extra fields we need
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
person_flavor: str,
|
||||
) -> None:
|
||||
_check_realm_user_update(var_name, event)
|
||||
|
||||
sub_type = PERSON_TYPES[person_flavor]
|
||||
validate_with_model(cast(dict[str, object], event["person"]), sub_type)
|
||||
|
||||
|
||||
def check_stream_update(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
) -> None:
|
||||
_check_stream_update(var_name, event)
|
||||
prop = event["property"]
|
||||
value = event["value"]
|
||||
|
||||
extra_keys = set(event.keys()) - {
|
||||
"id",
|
||||
"type",
|
||||
"op",
|
||||
"property",
|
||||
"value",
|
||||
"name",
|
||||
"stream_id",
|
||||
"first_message_id",
|
||||
"is_archived",
|
||||
"folder_id",
|
||||
}
|
||||
|
||||
if prop == "description":
|
||||
assert extra_keys == {"rendered_description"}
|
||||
assert isinstance(value, str)
|
||||
elif prop == "invite_only":
|
||||
assert extra_keys == {"history_public_to_subscribers", "is_web_public"}
|
||||
assert isinstance(value, bool)
|
||||
elif prop == "message_retention_days":
|
||||
assert extra_keys == set()
|
||||
if value is not None:
|
||||
assert isinstance(value, int)
|
||||
elif prop == "name":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, str)
|
||||
elif prop == "stream_post_policy":
|
||||
assert extra_keys == set()
|
||||
assert value in Stream.STREAM_POST_POLICY_TYPES
|
||||
elif prop in Stream.stream_permission_group_settings:
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, int | dict)
|
||||
# We cannot validate a TypedDict using isinstance, thus
|
||||
# requiring this check.
|
||||
if isinstance(value, dict):
|
||||
expected_keys = set(inspect.get_annotations(UserGroupMembersDict).keys())
|
||||
keys = set(value.keys())
|
||||
assert expected_keys == keys
|
||||
elif prop == "first_message_id":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, int)
|
||||
elif prop == "topics_policy":
|
||||
assert extra_keys == set()
|
||||
assert value in [e.name for e in StreamTopicsPolicyEnum]
|
||||
elif prop == "is_recently_active":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, bool)
|
||||
elif prop == "is_announcement_only":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, bool)
|
||||
elif prop == "is_archived":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, bool)
|
||||
elif prop == "folder_id":
|
||||
assert extra_keys == set()
|
||||
assert value is None or isinstance(value, int)
|
||||
elif prop == "default_push_notifications":
|
||||
assert extra_keys == set()
|
||||
assert isinstance(value, bool)
|
||||
else:
|
||||
raise AssertionError(f"Unknown property: {prop}")
|
||||
|
||||
|
||||
def check_subscription_update(
|
||||
var_name: str, event: dict[str, object], property: str, value: bool
|
||||
) -> None:
|
||||
_check_subscription_update(var_name, event)
|
||||
assert event["property"] == property
|
||||
assert event["value"] == value
|
||||
|
||||
|
||||
def check_user_settings_update(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
) -> None:
|
||||
_check_user_settings_update(var_name, event)
|
||||
setting_name = event["property"]
|
||||
value = event["value"]
|
||||
|
||||
assert isinstance(setting_name, str)
|
||||
if setting_name == "timezone":
|
||||
assert isinstance(value, str)
|
||||
else:
|
||||
setting_type = UserProfile.property_types[setting_name]
|
||||
if inspect.isclass(setting_type) and issubclass(setting_type, Enum):
|
||||
assert isinstance(value, str)
|
||||
setting_type[value]
|
||||
else:
|
||||
assert isinstance(value, setting_type)
|
||||
|
||||
if setting_name == "default_language":
|
||||
assert "language_name" in event
|
||||
else:
|
||||
assert "language_name" not in event
|
||||
|
||||
|
||||
def check_update_message(
|
||||
var_name: str,
|
||||
event: dict[str, object],
|
||||
is_stream_message: bool,
|
||||
has_content: bool,
|
||||
has_topic: bool,
|
||||
has_new_stream_id: bool,
|
||||
is_embedded_update_only: bool,
|
||||
) -> None:
|
||||
# Always check the basic schema first.
|
||||
_check_update_message(var_name, event)
|
||||
|
||||
actual_keys = set(event.keys())
|
||||
expected_keys = {
|
||||
"id",
|
||||
"type",
|
||||
"user_id",
|
||||
"edit_timestamp",
|
||||
"message_id",
|
||||
"flags",
|
||||
"message_ids",
|
||||
"rendering_only",
|
||||
}
|
||||
|
||||
if is_stream_message:
|
||||
expected_keys |= {
|
||||
"stream_id",
|
||||
"stream_name",
|
||||
}
|
||||
|
||||
if has_content:
|
||||
expected_keys |= {
|
||||
"is_me_message",
|
||||
"orig_content",
|
||||
"orig_rendered_content",
|
||||
"content",
|
||||
"rendered_content",
|
||||
}
|
||||
|
||||
if has_topic:
|
||||
expected_keys |= {
|
||||
"topic_links",
|
||||
ORIG_TOPIC,
|
||||
TOPIC_NAME,
|
||||
"propagate_mode",
|
||||
}
|
||||
|
||||
if has_new_stream_id:
|
||||
expected_keys |= {
|
||||
"new_stream_id",
|
||||
ORIG_TOPIC,
|
||||
"propagate_mode",
|
||||
}
|
||||
|
||||
if is_embedded_update_only:
|
||||
expected_keys |= {
|
||||
"content",
|
||||
"rendered_content",
|
||||
}
|
||||
assert event["user_id"] is None
|
||||
else:
|
||||
assert isinstance(event["user_id"], int)
|
||||
|
||||
assert event["rendering_only"] == is_embedded_update_only
|
||||
assert expected_keys == actual_keys
|
||||
|
||||
|
||||
def check_user_group_update(var_name: str, event: dict[str, object], fields: set[str]) -> None:
|
||||
_check_user_group_update(var_name, event)
|
||||
|
||||
assert isinstance(event["data"], dict)
|
||||
|
||||
assert set(event["data"].keys()) == fields
|
||||
|
||||
|
||||
def check_user_status(var_name: str, event: dict[str, object], fields: set[str]) -> None:
|
||||
_check_user_status(var_name, event)
|
||||
|
||||
assert set(event.keys()) == {"id", "type", "user_id"} | fields
|
||||
Reference in New Issue
Block a user