zulip/zerver/lib/event_schema.py

854 lines
23 KiB
Python
Raw Normal View History

"""
This is new module that we intend to GROW from test_events.py.
It will contain schemas (aka validators) for Zulip events.
Right now it's only intended to be used by test code.
"""
from typing import Callable, Dict, List, Sequence, Set, Tuple, Union
from zerver.lib.topic import ORIG_TOPIC, TOPIC_LINKS, TOPIC_NAME
from zerver.lib.validator import (
Validator,
check_bool,
2020-07-10 16:10:58 +02:00
check_dict,
check_dict_only,
check_int,
event_schema: Extract check_realm_user_update. This a pretty big commit, but I really wanted it to be atomic. All realm_user/update events look the same from the top: _check_realm_user_update = check_events_dict( required_keys=[ ("type", equals("realm_user")), ("op", equals("update")), ("person", _check_realm_user_person), ] ) And then we have a bunch of fields for person that are optional, and we usually only send user_id plus one other field, with the exception of avatar-related events: _check_realm_user_person = check_dict_only( required_keys=[ # vertical formatting ("user_id", check_int), ], optional_keys=[ ("avatar_source", check_string), ("avatar_url", check_none_or(check_string)), ("avatar_url_medium", check_none_or(check_string)), ("avatar_version", check_int), ("bot_owner_id", check_int), ("custom_profile_field", _check_custom_profile_field), ("delivery_email", check_string), ("full_name", check_string), ("role", check_int_in(UserProfile.ROLE_TYPES)), ("email", check_string), ("user_id", check_int), ("timezone", check_string), ], ) I would start the code review by just skimming the changes to event_schema.py, to get the big picture of the complexity here. Basically the schema is just the combined superset of all the individual schemas that we remove from test_events. Then I would read test_events.py. The simplest diffs are basically of this form: - schema_checker = check_events_dict([ - ('type', equals('realm_user')), - ('op', equals('update')), - ('person', check_dict_only([ - ('role', check_int_in(UserProfile.ROLE_TYPES)), - ('user_id', check_int), - ])), - ]) # ... - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) Instead of a custom schema checker, we use the "superset" schema checker, but then we pass in the set of fields that we expect to be there. Note that 'user_id' is always there. So most of the heavy lifting happens in this new function in event_schema.py: def check_realm_user_update( var_name: str, event: Dict[str, Any], optional_fields: Set[str], ) -> None: _check_realm_user_update(var_name, event) keys = set(event["person"].keys()) - {"user_id"} assert optional_fields == keys But we still do some more custom checks in test_events.py. custom profile fields: check keys of custom_profile_field def test_custom_profile_field_data_events(self) -> None: + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value", "rendered_value"} + ) + check_realm_user_update('events[0]', events[0], {"custom_profile_field"}) + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value"} + ) avatar fields: check more specific types, since the superset schema has check_none_or(check_string) def test_change_avatar_fields(self) -> None: + check_realm_user_update('events[0]', events[0], avatar_fields) + assert isinstance(events[0]['person']['avatar_url'], str) + assert isinstance(events[0]['person']['avatar_url_medium'], str) + check_realm_user_update('events[0]', events[0], avatar_fields) + self.assertEqual(events[0]['person']['avatar_url'], None) + self.assertEqual(events[0]['person']['avatar_url_medium'], None) Also note that avatar_fields is a set of four fields that are set in event_schema. full name: no extra work! def test_change_full_name(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'full_name'}) test_change_user_delivery_email_email_address_visibilty_admins: no extra work for delivery_email check avatar fields more directly roles (several examples) -- actually check the specific role def test_change_realm_authentication_methods(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) + self.assertEqual(events[0]['person']['role'], role) bot_owner_id: no extra work! - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) timezone: no extra work! - timezone_schema_checker('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"email", "timezone"})
2020-07-23 16:04:06 +02:00
check_int_in,
check_list,
check_none_or,
check_string,
check_union,
check_url,
equals,
)
from zerver.models import Realm, Stream, Subscription, UserProfile
# These fields are used for "stream" events, and are included in the
# larger "subscription" events that also contain personal settings.
basic_stream_fields = [
("description", check_string),
("first_message_id", check_none_or(check_int)),
("history_public_to_subscribers", check_bool),
("invite_only", check_bool),
("is_announcement_only", check_bool),
("is_web_public", check_bool),
("message_retention_days", equals(None)),
("name", check_string),
("rendered_description", check_string),
("stream_id", check_int),
("stream_post_policy", check_int),
("date_created", check_int),
]
subscription_fields: Sequence[Tuple[str, Validator[object]]] = [
*basic_stream_fields,
("audible_notifications", check_none_or(check_bool)),
("color", check_string),
("desktop_notifications", check_none_or(check_bool)),
("email_address", check_string),
("email_notifications", check_none_or(check_bool)),
("in_home_view", check_bool),
("is_muted", check_bool),
("role", check_int_in(Subscription.ROLE_TYPES)),
("pin_to_top", check_bool),
("push_notifications", check_none_or(check_bool)),
("stream_weekly_traffic", check_none_or(check_int)),
("wildcard_mentions_notify", check_none_or(check_bool)),
]
def check_events_dict(
required_keys: Sequence[Tuple[str, Validator[object]]],
optional_keys: Sequence[Tuple[str, Validator[object]]] = [],
) -> Validator[Dict[str, object]]:
"""
This is just a tiny wrapper on check_dict, but it provides
some minor benefits:
- mark clearly that the schema is for a Zulip event
- make sure there's a type field
- add id field automatically
- sanity check that we have no duplicate keys (we
should just make check_dict do that, eventually)
"""
rkeys = [key[0] for key in required_keys]
okeys = [key[0] for key in optional_keys]
keys = rkeys + okeys
assert len(keys) == len(set(keys))
assert "type" in rkeys
assert "id" not in keys
return check_dict_only(
required_keys=list(required_keys) + [("id", check_int)],
optional_keys=optional_keys,
)
check_add_or_remove = check_union(
[
# force vertical
equals("add"),
equals("remove"),
]
)
check_value = check_union(
[
# force vertical formatting
check_bool,
check_int,
check_string,
]
)
check_optional_value = check_union(
[
# force vertical formatting
check_bool,
check_int,
check_string,
equals(None),
]
)
check_alert_words = check_events_dict(
required_keys=[
# force vertical formatting
("type", equals("alert_words")),
("alert_words", check_list(check_string)),
]
)
_check_custom_profile_field = check_dict_only(
required_keys=[
("id", check_int),
("type", check_int),
("name", check_string),
("hint", check_string),
("field_data", check_string),
("order", check_int),
]
)
check_custom_profile_fields = check_events_dict(
required_keys=[
("type", equals("custom_profile_fields")),
("op", equals("add")),
("fields", check_list(_check_custom_profile_field)),
]
)
_check_stream_group = check_dict_only(
required_keys=[
("name", check_string),
("id", check_int),
("description", check_string),
("streams", check_list(check_dict_only(basic_stream_fields))),
]
)
check_default_stream_groups = check_events_dict(
required_keys=[
# force vertical
("type", equals("default_stream_groups")),
("default_stream_groups", check_list(_check_stream_group)),
]
)
check_default_streams = check_events_dict(
required_keys=[
("type", equals("default_streams")),
("default_streams", check_list(check_dict_only(basic_stream_fields))),
]
)
check_invites_changed = check_events_dict(
required_keys=[
# the most boring event...no metadata
("type", equals("invites_changed")),
]
)
2020-07-10 16:10:58 +02:00
message_fields = [
("avatar_url", check_none_or(check_string)),
("client", check_string),
("content", check_string),
("content_type", equals("text/html")),
("display_recipient", check_string),
("id", check_int),
("is_me_message", check_bool),
("reactions", check_list(check_dict([]))),
("recipient_id", check_int),
("sender_realm_str", check_string),
("sender_email", check_string),
("sender_full_name", check_string),
("sender_id", check_int),
("stream_id", check_int),
(TOPIC_NAME, check_string),
(TOPIC_LINKS, check_list(check_string)),
("submessages", check_list(check_dict([]))),
("timestamp", check_int),
("type", check_string),
]
check_message = check_events_dict(
required_keys=[
("type", equals("message")),
("flags", check_list(check_string)),
("message", check_dict_only(message_fields)),
]
)
2020-07-17 09:23:12 +02:00
# We will eventually just send user_ids.
_check_reaction_user = check_dict_only(
required_keys=[
# force vertical
("email", check_string),
("full_name", check_string),
("user_id", check_int),
]
)
_check_reaction = check_events_dict(
required_keys=[
("type", equals("reaction")),
("op", check_add_or_remove),
("message_id", check_int),
("emoji_name", check_string),
("emoji_code", check_string),
("reaction_type", check_string),
("user_id", check_int),
("user", _check_reaction_user),
]
)
def check_reaction(var_name: str, event: Dict[str, object], op: str) -> None:
2020-07-17 09:23:12 +02:00
_check_reaction(var_name, event)
assert event["op"] == op
_check_bot_services_outgoing = check_dict_only(
required_keys=[
# force vertical
("base_url", check_url),
("interface", check_int),
("token", check_string),
]
)
# We use a strict check here, because our tests
# don't specifically focus on seeing how
# flexible we can make the types be for config_data.
_ad_hoc_config_data_schema = equals(dict(foo="bar"))
_check_bot_services_embedded = check_dict_only(
required_keys=[
# force vertical
("service_name", check_string),
("config_data", _ad_hoc_config_data_schema),
]
)
# Note that regular bots just get an empty list of services,
# so the sub_validator for check_list won't matter for them.
_check_bot_services = check_list(
check_union(
[
# force vertical
_check_bot_services_outgoing,
_check_bot_services_embedded,
]
),
)
_check_bot = check_dict_only(
required_keys=[
("user_id", check_int),
("api_key", check_string),
("avatar_url", check_string),
("bot_type", check_int),
("default_all_public_streams", check_bool),
("default_events_register_stream", check_none_or(check_string)),
("default_sending_stream", check_none_or(check_string)),
("email", check_string),
("full_name", check_string),
("is_active", check_bool),
("owner_id", check_int),
("services", _check_bot_services),
]
)
_check_realm_bot_add = check_events_dict(
required_keys=[
# force vertical
("type", equals("realm_bot")),
("op", equals("add")),
("bot", _check_bot),
]
)
def check_realm_bot_add(var_name: str, event: Dict[str, object],) -> None:
_check_realm_bot_add(var_name, event)
assert isinstance(event["bot"], dict)
bot_type = event["bot"]["bot_type"]
services_field = f"{var_name}['bot']['services']"
services = event["bot"]["services"]
if bot_type == UserProfile.DEFAULT_BOT:
equals([])(services_field, services)
elif bot_type == UserProfile.OUTGOING_WEBHOOK_BOT:
check_list(_check_bot_services_outgoing, length=1)(services_field, services)
elif bot_type == UserProfile.EMBEDDED_BOT:
check_list(_check_bot_services_embedded, length=1)(services_field, services)
else:
raise AssertionError(f"Unknown bot_type: {bot_type}")
_check_bot_for_delete = check_dict_only(
required_keys=[
# for legacy reasons we have a dict here
# with only one key
("user_id", check_int),
]
)
check_realm_bot_delete = check_events_dict(
required_keys=[
("type", equals("realm_bot")),
("op", equals("delete")),
("bot", _check_bot_for_delete),
]
)
_check_bot_for_remove = check_dict_only(
required_keys=[
# Why does remove have full_name but delete doesn't?
# Why do we have both a remove and a delete event
# for bots? I don't know the answer as I write this.
("full_name", check_string),
("user_id", check_int),
]
)
check_realm_bot_remove = check_events_dict(
required_keys=[
("type", equals("realm_bot")),
("op", equals("remove")),
("bot", _check_bot_for_remove),
]
)
_check_bot_for_update = check_dict_only(
required_keys=[
# force vertical
("user_id", check_int),
],
optional_keys=[
("api_key", check_string),
("avatar_url", check_string),
("default_all_public_streams", check_bool),
("default_events_register_stream", check_none_or(check_string)),
("default_sending_stream", check_none_or(check_string)),
("full_name", check_string),
("owner_id", check_int),
("services", _check_bot_services),
],
)
_check_realm_bot_update = check_events_dict(
required_keys=[
("type", equals("realm_bot")),
("op", equals("update")),
("bot", _check_bot_for_update),
]
)
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())
_check_plan_type_extra_data = check_dict_only(
required_keys=[
# force vertical
("upload_quota", check_int),
]
)
"""
realm/update events are flexible for values;
we will use a more strict checker to check
types in a context-specific manner
"""
_check_realm_update = check_events_dict(
required_keys=[
("type", equals("realm")),
("op", equals("update")),
("property", check_string),
("value", check_value),
],
optional_keys=[
# force vertical
("extra_data", _check_plan_type_extra_data),
],
)
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.
"""
_check_realm_update(var_name, event)
assert prop == event["property"]
value = event["value"]
if prop == "plan_type":
assert isinstance(value, int)
assert "extra_data" in event.keys()
return
assert "extra_data" not in event.keys()
if prop in ["notifications_stream_id", "signup_notifications_stream_id"]:
assert isinstance(value, int)
return
property_type = Realm.property_types[prop]
if property_type in (bool, int, str):
assert isinstance(value, property_type)
elif property_type == (int, type(None)):
assert isinstance(value, int)
elif property_type == (str, type(None)):
assert isinstance(value, str)
else:
raise AssertionError(f"Unexpected property type {property_type}")
event_schema: Extract check_realm_user_update. This a pretty big commit, but I really wanted it to be atomic. All realm_user/update events look the same from the top: _check_realm_user_update = check_events_dict( required_keys=[ ("type", equals("realm_user")), ("op", equals("update")), ("person", _check_realm_user_person), ] ) And then we have a bunch of fields for person that are optional, and we usually only send user_id plus one other field, with the exception of avatar-related events: _check_realm_user_person = check_dict_only( required_keys=[ # vertical formatting ("user_id", check_int), ], optional_keys=[ ("avatar_source", check_string), ("avatar_url", check_none_or(check_string)), ("avatar_url_medium", check_none_or(check_string)), ("avatar_version", check_int), ("bot_owner_id", check_int), ("custom_profile_field", _check_custom_profile_field), ("delivery_email", check_string), ("full_name", check_string), ("role", check_int_in(UserProfile.ROLE_TYPES)), ("email", check_string), ("user_id", check_int), ("timezone", check_string), ], ) I would start the code review by just skimming the changes to event_schema.py, to get the big picture of the complexity here. Basically the schema is just the combined superset of all the individual schemas that we remove from test_events. Then I would read test_events.py. The simplest diffs are basically of this form: - schema_checker = check_events_dict([ - ('type', equals('realm_user')), - ('op', equals('update')), - ('person', check_dict_only([ - ('role', check_int_in(UserProfile.ROLE_TYPES)), - ('user_id', check_int), - ])), - ]) # ... - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) Instead of a custom schema checker, we use the "superset" schema checker, but then we pass in the set of fields that we expect to be there. Note that 'user_id' is always there. So most of the heavy lifting happens in this new function in event_schema.py: def check_realm_user_update( var_name: str, event: Dict[str, Any], optional_fields: Set[str], ) -> None: _check_realm_user_update(var_name, event) keys = set(event["person"].keys()) - {"user_id"} assert optional_fields == keys But we still do some more custom checks in test_events.py. custom profile fields: check keys of custom_profile_field def test_custom_profile_field_data_events(self) -> None: + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value", "rendered_value"} + ) + check_realm_user_update('events[0]', events[0], {"custom_profile_field"}) + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value"} + ) avatar fields: check more specific types, since the superset schema has check_none_or(check_string) def test_change_avatar_fields(self) -> None: + check_realm_user_update('events[0]', events[0], avatar_fields) + assert isinstance(events[0]['person']['avatar_url'], str) + assert isinstance(events[0]['person']['avatar_url_medium'], str) + check_realm_user_update('events[0]', events[0], avatar_fields) + self.assertEqual(events[0]['person']['avatar_url'], None) + self.assertEqual(events[0]['person']['avatar_url_medium'], None) Also note that avatar_fields is a set of four fields that are set in event_schema. full name: no extra work! def test_change_full_name(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'full_name'}) test_change_user_delivery_email_email_address_visibilty_admins: no extra work for delivery_email check avatar fields more directly roles (several examples) -- actually check the specific role def test_change_realm_authentication_methods(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) + self.assertEqual(events[0]['person']['role'], role) bot_owner_id: no extra work! - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) timezone: no extra work! - timezone_schema_checker('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"email", "timezone"})
2020-07-23 16:04:06 +02:00
avatar_fields = {
"avatar_source",
"avatar_url",
"avatar_url_medium",
"avatar_version",
}
_check_custom_profile_field = check_dict_only(
required_keys=[
# vertical formatting
("id", check_int),
("value", check_string),
],
optional_keys=[
# vertical formatting
("rendered_value", check_string),
],
)
_check_realm_user_person = check_dict_only(
required_keys=[
# vertical formatting
("user_id", check_int),
],
optional_keys=[
("avatar_source", check_string),
("avatar_url", check_none_or(check_string)),
("avatar_url_medium", check_none_or(check_string)),
("avatar_version", check_int),
("bot_owner_id", check_int),
("custom_profile_field", _check_custom_profile_field),
("delivery_email", check_string),
("full_name", check_string),
("role", check_int_in(UserProfile.ROLE_TYPES)),
("email", check_string),
("user_id", check_int),
("timezone", check_string),
],
)
_check_realm_user_update = check_events_dict(
required_keys=[
("type", equals("realm_user")),
("op", equals("update")),
("person", _check_realm_user_person),
]
)
def check_realm_user_update(
var_name: str, event: Dict[str, object], optional_fields: Set[str],
event_schema: Extract check_realm_user_update. This a pretty big commit, but I really wanted it to be atomic. All realm_user/update events look the same from the top: _check_realm_user_update = check_events_dict( required_keys=[ ("type", equals("realm_user")), ("op", equals("update")), ("person", _check_realm_user_person), ] ) And then we have a bunch of fields for person that are optional, and we usually only send user_id plus one other field, with the exception of avatar-related events: _check_realm_user_person = check_dict_only( required_keys=[ # vertical formatting ("user_id", check_int), ], optional_keys=[ ("avatar_source", check_string), ("avatar_url", check_none_or(check_string)), ("avatar_url_medium", check_none_or(check_string)), ("avatar_version", check_int), ("bot_owner_id", check_int), ("custom_profile_field", _check_custom_profile_field), ("delivery_email", check_string), ("full_name", check_string), ("role", check_int_in(UserProfile.ROLE_TYPES)), ("email", check_string), ("user_id", check_int), ("timezone", check_string), ], ) I would start the code review by just skimming the changes to event_schema.py, to get the big picture of the complexity here. Basically the schema is just the combined superset of all the individual schemas that we remove from test_events. Then I would read test_events.py. The simplest diffs are basically of this form: - schema_checker = check_events_dict([ - ('type', equals('realm_user')), - ('op', equals('update')), - ('person', check_dict_only([ - ('role', check_int_in(UserProfile.ROLE_TYPES)), - ('user_id', check_int), - ])), - ]) # ... - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) Instead of a custom schema checker, we use the "superset" schema checker, but then we pass in the set of fields that we expect to be there. Note that 'user_id' is always there. So most of the heavy lifting happens in this new function in event_schema.py: def check_realm_user_update( var_name: str, event: Dict[str, Any], optional_fields: Set[str], ) -> None: _check_realm_user_update(var_name, event) keys = set(event["person"].keys()) - {"user_id"} assert optional_fields == keys But we still do some more custom checks in test_events.py. custom profile fields: check keys of custom_profile_field def test_custom_profile_field_data_events(self) -> None: + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value", "rendered_value"} + ) + check_realm_user_update('events[0]', events[0], {"custom_profile_field"}) + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value"} + ) avatar fields: check more specific types, since the superset schema has check_none_or(check_string) def test_change_avatar_fields(self) -> None: + check_realm_user_update('events[0]', events[0], avatar_fields) + assert isinstance(events[0]['person']['avatar_url'], str) + assert isinstance(events[0]['person']['avatar_url_medium'], str) + check_realm_user_update('events[0]', events[0], avatar_fields) + self.assertEqual(events[0]['person']['avatar_url'], None) + self.assertEqual(events[0]['person']['avatar_url_medium'], None) Also note that avatar_fields is a set of four fields that are set in event_schema. full name: no extra work! def test_change_full_name(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'full_name'}) test_change_user_delivery_email_email_address_visibilty_admins: no extra work for delivery_email check avatar fields more directly roles (several examples) -- actually check the specific role def test_change_realm_authentication_methods(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) + self.assertEqual(events[0]['person']['role'], role) bot_owner_id: no extra work! - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) timezone: no extra work! - timezone_schema_checker('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"email", "timezone"})
2020-07-23 16:04:06 +02:00
) -> None:
_check_realm_user_update(var_name, event)
assert isinstance(event["person"], dict)
event_schema: Extract check_realm_user_update. This a pretty big commit, but I really wanted it to be atomic. All realm_user/update events look the same from the top: _check_realm_user_update = check_events_dict( required_keys=[ ("type", equals("realm_user")), ("op", equals("update")), ("person", _check_realm_user_person), ] ) And then we have a bunch of fields for person that are optional, and we usually only send user_id plus one other field, with the exception of avatar-related events: _check_realm_user_person = check_dict_only( required_keys=[ # vertical formatting ("user_id", check_int), ], optional_keys=[ ("avatar_source", check_string), ("avatar_url", check_none_or(check_string)), ("avatar_url_medium", check_none_or(check_string)), ("avatar_version", check_int), ("bot_owner_id", check_int), ("custom_profile_field", _check_custom_profile_field), ("delivery_email", check_string), ("full_name", check_string), ("role", check_int_in(UserProfile.ROLE_TYPES)), ("email", check_string), ("user_id", check_int), ("timezone", check_string), ], ) I would start the code review by just skimming the changes to event_schema.py, to get the big picture of the complexity here. Basically the schema is just the combined superset of all the individual schemas that we remove from test_events. Then I would read test_events.py. The simplest diffs are basically of this form: - schema_checker = check_events_dict([ - ('type', equals('realm_user')), - ('op', equals('update')), - ('person', check_dict_only([ - ('role', check_int_in(UserProfile.ROLE_TYPES)), - ('user_id', check_int), - ])), - ]) # ... - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) Instead of a custom schema checker, we use the "superset" schema checker, but then we pass in the set of fields that we expect to be there. Note that 'user_id' is always there. So most of the heavy lifting happens in this new function in event_schema.py: def check_realm_user_update( var_name: str, event: Dict[str, Any], optional_fields: Set[str], ) -> None: _check_realm_user_update(var_name, event) keys = set(event["person"].keys()) - {"user_id"} assert optional_fields == keys But we still do some more custom checks in test_events.py. custom profile fields: check keys of custom_profile_field def test_custom_profile_field_data_events(self) -> None: + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value", "rendered_value"} + ) + check_realm_user_update('events[0]', events[0], {"custom_profile_field"}) + self.assertEqual( + events[0]['person']['custom_profile_field'].keys(), + {"id", "value"} + ) avatar fields: check more specific types, since the superset schema has check_none_or(check_string) def test_change_avatar_fields(self) -> None: + check_realm_user_update('events[0]', events[0], avatar_fields) + assert isinstance(events[0]['person']['avatar_url'], str) + assert isinstance(events[0]['person']['avatar_url_medium'], str) + check_realm_user_update('events[0]', events[0], avatar_fields) + self.assertEqual(events[0]['person']['avatar_url'], None) + self.assertEqual(events[0]['person']['avatar_url_medium'], None) Also note that avatar_fields is a set of four fields that are set in event_schema. full name: no extra work! def test_change_full_name(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'full_name'}) test_change_user_delivery_email_email_address_visibilty_admins: no extra work for delivery_email check avatar fields more directly roles (several examples) -- actually check the specific role def test_change_realm_authentication_methods(self) -> None: - schema_checker('events[0]', events[0]) + check_realm_user_update('events[0]', events[0], {'role'}) + self.assertEqual(events[0]['person']['role'], role) bot_owner_id: no extra work! - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) - change_bot_owner_checker_user('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"bot_owner_id"}) timezone: no extra work! - timezone_schema_checker('events[1]', events[1]) + check_realm_user_update('events[1]', events[1], {"email", "timezone"})
2020-07-23 16:04:06 +02:00
keys = set(event["person"].keys()) - {"user_id"}
assert optional_fields == keys
check_stream_create = check_events_dict(
required_keys=[
("type", equals("stream")),
("op", equals("create")),
("streams", check_list(check_dict_only(basic_stream_fields))),
]
)
check_stream_delete = check_events_dict(
required_keys=[
("type", equals("stream")),
("op", equals("delete")),
("streams", check_list(check_dict_only(basic_stream_fields))),
]
)
_check_stream_update = check_events_dict(
required_keys=[
("type", equals("stream")),
("op", equals("update")),
("property", check_string),
("value", check_optional_value),
("name", check_string),
("stream_id", check_int),
],
optional_keys=[
("rendered_description", check_string),
("history_public_to_subscribers", check_bool),
],
)
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",
}
if prop == "description":
assert extra_keys == {"rendered_description"}
assert isinstance(value, str)
elif prop == "email_address":
assert extra_keys == set()
assert isinstance(value, str)
elif prop == "invite_only":
assert extra_keys == {"history_public_to_subscribers"}
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
else:
raise AssertionError(f"Unknown property: {prop}")
check_submessage = check_events_dict(
required_keys=[
("type", equals("submessage")),
("message_id", check_int),
("submessage_id", check_int),
("sender_id", check_int),
("msg_type", check_string),
("content", check_string),
]
)
_check_single_subscription = check_dict_only(
required_keys=subscription_fields,
optional_keys=[
# force vertical
("subscribers", check_list(check_int)),
],
)
_check_subscription_add = check_events_dict(
required_keys=[
("type", equals("subscription")),
("op", equals("add")),
("subscriptions", check_list(_check_single_subscription)),
],
)
def check_subscription_add(
var_name: str, event: Dict[str, object], include_subscribers: bool,
) -> None:
_check_subscription_add(var_name, event)
assert isinstance(event["subscriptions"], list)
for sub in event["subscriptions"]:
if include_subscribers:
assert "subscribers" in sub.keys()
else:
assert "subscribers" not in sub.keys()
check_subscription_peer_add = check_events_dict(
required_keys=[
("type", equals("subscription")),
("op", equals("peer_add")),
("user_id", check_int),
("stream_id", check_int),
]
)
check_subscription_peer_remove = check_events_dict(
required_keys=[
("type", equals("subscription")),
("op", equals("peer_remove")),
("user_id", check_int),
("stream_id", check_int),
]
)
_check_remove_sub = check_dict_only(
required_keys=[
# We should eventually just return stream_id here.
("name", check_string),
("stream_id", check_int),
]
)
check_subscription_remove = check_events_dict(
required_keys=[
("type", equals("subscription")),
("op", equals("remove")),
("subscriptions", check_list(_check_remove_sub)),
]
)
_check_typing_person = check_dict_only(
required_keys=[
# we should eventually just send user_id
("email", check_string),
("user_id", check_int),
]
)
check_typing_start = check_events_dict(
required_keys=[
("type", equals("typing")),
("op", equals("start")),
("sender", _check_typing_person),
("recipients", check_list(_check_typing_person)),
]
)
_check_update_display_settings = check_events_dict(
required_keys=[
("type", equals("update_display_settings")),
("setting_name", check_string),
("setting", check_value),
("user", check_string),
],
optional_keys=[
# force vertical
("language_name", check_string),
],
)
def check_update_display_settings(var_name: str, event: Dict[str, object],) -> None:
"""
Display setting events have a "setting" field that
is more specifically typed according to the
UserProfile.property_types dictionary.
"""
_check_update_display_settings(var_name, event)
setting_name = event["setting_name"]
setting = event["setting"]
assert isinstance(setting_name, str)
setting_type = UserProfile.property_types[setting_name]
assert isinstance(setting, setting_type)
if setting_name == "default_language":
assert "language_name" in event.keys()
else:
assert "language_name" not in event.keys()
_check_update_global_notifications = check_events_dict(
required_keys=[
("type", equals("update_global_notifications")),
("notification_name", check_string),
("setting", check_value),
("user", check_string),
]
)
def check_update_global_notifications(
var_name: str, event: Dict[str, object], desired_val: Union[bool, int, str],
) -> None:
"""
See UserProfile.notification_setting_types for
more details.
"""
_check_update_global_notifications(var_name, event)
setting_name = event["notification_name"]
setting = event["setting"]
assert setting == desired_val
assert isinstance(setting_name, str)
setting_type = UserProfile.notification_setting_types[setting_name]
assert isinstance(setting, setting_type)
update_message_required_fields = [
("type", equals("update_message")),
("user_id", check_int),
("edit_timestamp", check_int),
("message_id", check_int),
]
update_message_content_fields: List[Tuple[str, Callable[[str, object], object]]] = [
("content", check_string),
("is_me_message", check_bool),
("orig_content", check_string),
("orig_rendered_content", check_string),
("prev_rendered_content_version", check_int),
("rendered_content", check_string),
]
update_message_topic_fields = [
("flags", check_list(check_string)),
("message_ids", check_list(check_int)),
("new_stream_id", check_int),
(ORIG_TOPIC, check_string),
("propagate_mode", check_string),
("stream_id", check_int),
("stream_name", check_string),
(TOPIC_LINKS, check_list(check_string)),
(TOPIC_NAME, check_string),
]
update_message_optional_fields = (
update_message_content_fields + update_message_topic_fields
)
# The schema here does not include the "embedded"
# variant of update_message; it is for message
# and topic editing.
_check_update_message = check_events_dict(
required_keys=update_message_required_fields,
optional_keys=update_message_optional_fields,
)
def check_update_message(
var_name: str,
event: Dict[str, object],
has_content: bool,
has_topic: bool,
has_new_stream_id: bool,
) -> None:
# Always check the basic schema first.
_check_update_message(var_name, event)
actual_keys = set(event.keys())
expected_keys = {"id"}
expected_keys.update(tup[0] for tup in update_message_required_fields)
if has_content:
expected_keys.update(tup[0] for tup in update_message_content_fields)
if has_topic:
expected_keys.update(tup[0] for tup in update_message_topic_fields)
if not has_new_stream_id:
expected_keys.discard("new_stream_id")
assert expected_keys == actual_keys
check_update_message_embedded = check_events_dict(
required_keys=[
("type", equals("update_message")),
("flags", check_list(check_string)),
("content", check_string),
("message_id", check_int),
("message_ids", check_list(check_int)),
("rendered_content", check_string),
]
)
_check_update_message_flags = check_events_dict(
required_keys=[
("type", equals("update_message_flags")),
("operation", check_add_or_remove),
("flag", check_string),
("messages", check_list(check_int)),
("all", check_bool),
]
)
def check_update_message_flags(
var_name: str, event: Dict[str, object], operation: str
) -> None:
_check_update_message_flags(var_name, event)
assert event["operation"] == operation
_check_group = check_dict_only(
required_keys=[
("id", check_int),
("name", check_string),
("members", check_list(check_int)),
("description", check_string),
]
)
check_user_group_add = check_events_dict(
required_keys=[
("type", equals("user_group")),
("op", equals("add")),
("group", _check_group),
]
)
check_user_status = check_events_dict(
required_keys=[
("type", equals("user_status")),
("user_id", check_int),
("away", check_bool),
("status_text", check_string),
]
)