mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Note that we use the actual integer bot_type value now to determine how we validate services.
		
			
				
	
	
		
			405 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			405 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
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 Any, Dict, Sequence, Tuple, Union
 | 
						|
 | 
						|
from zerver.lib.validator import (
 | 
						|
    Validator,
 | 
						|
    check_bool,
 | 
						|
    check_dict_only,
 | 
						|
    check_int,
 | 
						|
    check_list,
 | 
						|
    check_none_or,
 | 
						|
    check_string,
 | 
						|
    check_union,
 | 
						|
    check_url,
 | 
						|
    equals,
 | 
						|
)
 | 
						|
from zerver.models import Realm, Stream, 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),
 | 
						|
]
 | 
						|
 | 
						|
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),
 | 
						|
    ("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_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_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, Any],) -> None:
 | 
						|
    _check_realm_bot_add(var_name, event)
 | 
						|
 | 
						|
    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}")
 | 
						|
 | 
						|
 | 
						|
"""
 | 
						|
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),
 | 
						|
    ]
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
def check_realm_update(var_name: str, event: Dict[str, Any],) -> 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)
 | 
						|
    prop = event["property"]
 | 
						|
    value = event["value"]
 | 
						|
 | 
						|
    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}")
 | 
						|
 | 
						|
 | 
						|
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_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, Any],) -> 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_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, Any], include_subscribers: bool,
 | 
						|
) -> None:
 | 
						|
    _check_subscription_add(var_name, event)
 | 
						|
 | 
						|
    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_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, Any],) -> 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"]
 | 
						|
 | 
						|
    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, Any], 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
 | 
						|
 | 
						|
    setting_type = UserProfile.notification_setting_types[setting_name]
 | 
						|
    assert isinstance(setting, setting_type)
 |