5aef823807
- Removed the restricted data cache (it wasn't used since OS 3.0) - unify functions for restricted and full data: Just one function, which accteps an optional user_id: If it is None, full data is returned, and with a user id given, the restricted data - More atomic access to redis, especially for: - Check for data-existance in redis and do an auto-ensure-cache. - Speedup through hashing of scripts and redis' script cache. - Save schema version into the redis cache and rebuild, if the version changed Client changes: - Simplified the ConstantsService - Fixed bug, when receiving an autoupdate with all_data=True from the Server
565 lines
18 KiB
Python
565 lines
18 KiB
Python
import asyncio
|
|
from importlib import import_module
|
|
from typing import Optional
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from asgiref.sync import sync_to_async
|
|
from django.conf import settings
|
|
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
|
|
|
from openslides.asgi import application
|
|
from openslides.core.config import config
|
|
from openslides.utils.autoupdate import (
|
|
Element,
|
|
inform_changed_elements,
|
|
inform_deleted_data,
|
|
)
|
|
from openslides.utils.cache import element_cache
|
|
from openslides.utils.websocket import (
|
|
WEBSOCKET_CHANGE_ID_TOO_HIGH,
|
|
WEBSOCKET_WRONG_FORMAT,
|
|
)
|
|
|
|
from ...unit.utils.cache_provider import Collection1, Collection2, get_cachable_provider
|
|
from ..helpers import TConfig, TProjector, TUser
|
|
from ..websocket import WebsocketCommunicator
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def prepare_element_cache(settings):
|
|
"""
|
|
Resets the element cache.
|
|
|
|
Uses a cacheable_provider for tests with example data.
|
|
"""
|
|
await element_cache.cache_provider.clear_cache()
|
|
orig_cachable_provider = element_cache.cachable_provider
|
|
element_cache.cachable_provider = get_cachable_provider(
|
|
[Collection1(), Collection2(), TConfig(), TUser(), TProjector()]
|
|
)
|
|
element_cache._cachables = None
|
|
await element_cache.async_ensure_cache(default_change_id=1)
|
|
yield
|
|
# Reset the cachable_provider
|
|
element_cache.cachable_provider = orig_cachable_provider
|
|
element_cache._cachables = None
|
|
await element_cache.cache_provider.clear_cache()
|
|
|
|
|
|
@pytest.fixture
|
|
async def get_communicator():
|
|
communicator: Optional[WebsocketCommunicator] = None
|
|
|
|
def get_communicator(query_string=""):
|
|
nonlocal communicator # use the outer communicator variable
|
|
if query_string:
|
|
query_string = f"?{query_string}"
|
|
communicator = WebsocketCommunicator(application, f"/ws/{query_string}")
|
|
return communicator
|
|
|
|
yield get_communicator
|
|
if communicator:
|
|
await communicator.disconnect()
|
|
|
|
|
|
@pytest.fixture
|
|
async def communicator(get_communicator):
|
|
yield get_communicator()
|
|
|
|
|
|
@pytest.fixture
|
|
async def set_config():
|
|
"""
|
|
Set a config variable in the element_cache without hitting the database.
|
|
"""
|
|
|
|
async def _set_config(key, value):
|
|
with patch("openslides.utils.autoupdate.save_history"):
|
|
collection_string = config.get_collection_string()
|
|
config_id = config.key_to_id[key] # type: ignore
|
|
full_data = {"id": config_id, "key": key, "value": value}
|
|
await sync_to_async(inform_changed_elements)(
|
|
[
|
|
Element(
|
|
id=config_id,
|
|
collection_string=collection_string,
|
|
full_data=full_data,
|
|
disable_history=True,
|
|
)
|
|
]
|
|
)
|
|
|
|
return _set_config
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normal_connection(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
connected, __ = await get_communicator().connect()
|
|
assert connected
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_with_change_id(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("change_id=0")
|
|
await communicator.connect()
|
|
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert "changed" in content
|
|
assert "deleted" in content
|
|
assert "from_change_id" in content
|
|
assert "to_change_id" in content
|
|
assert Collection1().get_collection_string() in content["changed"]
|
|
assert Collection2().get_collection_string() in content["changed"]
|
|
assert TConfig().get_collection_string() in content["changed"]
|
|
assert TUser().get_collection_string() in content["changed"]
|
|
|
|
|
|
@pytest.mark.xfail # This will fail until a proper solution in #4009
|
|
@pytest.mark.asyncio
|
|
async def test_connection_with_invalid_change_id(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("change_id=invalid")
|
|
connected, __ = await communicator.connect()
|
|
|
|
assert connected is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connection_with_too_big_change_id(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("change_id=100")
|
|
|
|
connected, __ = await communicator.connect()
|
|
|
|
assert connected is True
|
|
await communicator.assert_receive_error(code=WEBSOCKET_CHANGE_ID_TOO_HIGH)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_changed_data_autoupdate_off(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
# Change a config value
|
|
await set_config("general_event_name", "Test Event")
|
|
assert await communicator.receive_nothing()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_changed_data_autoupdate_on(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("autoupdate=on")
|
|
await communicator.connect()
|
|
|
|
# Change a config value
|
|
await set_config("general_event_name", "Test Event")
|
|
response = await communicator.receive_json_from()
|
|
|
|
id = config.get_key_to_id()["general_event_name"]
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert content["changed"] == {
|
|
"core/config": [{"id": id, "key": "general_event_name", "value": "Test Event"}]
|
|
}
|
|
|
|
|
|
@pytest.mark.xfail # This will fail until a proper solution in #4009
|
|
@pytest.mark.asyncio
|
|
async def test_anonymous_disabled(communicator):
|
|
connected, __ = await communicator.connect()
|
|
|
|
assert not connected
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_with_user():
|
|
# login user with id 1
|
|
engine = import_module(settings.SESSION_ENGINE)
|
|
session = engine.SessionStore() # type: ignore
|
|
session[SESSION_KEY] = "1"
|
|
session[
|
|
HASH_SESSION_KEY
|
|
] = "362d4f2de1463293cb3aaba7727c967c35de43ee" # see helpers.TUser
|
|
session[BACKEND_SESSION_KEY] = "django.contrib.auth.backends.ModelBackend"
|
|
session.save()
|
|
scn = settings.SESSION_COOKIE_NAME
|
|
cookies = (b"cookie", f"{scn}={session.session_key}".encode())
|
|
communicator = WebsocketCommunicator(application, "/ws/", headers=[cookies])
|
|
|
|
connected, __ = await communicator.connect()
|
|
|
|
assert connected
|
|
|
|
await communicator.disconnect()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_receive_deleted_data(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("autoupdate=on")
|
|
await communicator.connect()
|
|
|
|
# Delete test element
|
|
with patch("openslides.utils.autoupdate.save_history"):
|
|
await sync_to_async(inform_deleted_data)(
|
|
[(Collection1().get_collection_string(), 1)]
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert content["deleted"] == {Collection1().get_collection_string(): [1]}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_notify(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "notify",
|
|
"content": {"content": "foobar, what else.", "name": "message_name"},
|
|
"id": "test",
|
|
}
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
content = response["content"]
|
|
assert isinstance(content, dict)
|
|
assert content["content"] == "foobar, what else."
|
|
assert content["name"] == "message_name"
|
|
assert "senderChannelName" in content
|
|
assert content["senderUserId"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_websocket_message_type(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to([])
|
|
|
|
await communicator.assert_receive_error(code=WEBSOCKET_WRONG_FORMAT)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_websocket_message_no_id(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to({"type": "test", "content": "foobar"})
|
|
|
|
await communicator.assert_receive_error(code=WEBSOCKET_WRONG_FORMAT)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_unknown_type(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "if_you_add_this_type_to_openslides_I_will_be_sad",
|
|
"content": True,
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
|
|
await communicator.assert_receive_error(
|
|
code=WEBSOCKET_WRONG_FORMAT, in_response="test_id"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_constants(communicator, settings, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "constants", "content": "", "id": "test_id"}
|
|
)
|
|
|
|
response = await communicator.receive_json_from()
|
|
assert response["type"] == "constants"
|
|
# See conftest.py for the content of 'content'
|
|
assert response["content"] == {"constant1": "value1", "constant2": "value2"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_get_elements(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {}, "id": "test_id"}
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert "changed" in content
|
|
assert "deleted" in content
|
|
assert "from_change_id" in content
|
|
assert "to_change_id" in content
|
|
assert Collection1().get_collection_string() in content["changed"]
|
|
assert Collection2().get_collection_string() in content["changed"]
|
|
assert TConfig().get_collection_string() in content["changed"]
|
|
assert TUser().get_collection_string() in content["changed"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_get_elements_too_big_change_id(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": 100}, "id": "test_id"}
|
|
)
|
|
await communicator.assert_receive_error(
|
|
code=WEBSOCKET_CHANGE_ID_TOO_HIGH, in_response="test_id"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_get_elements_to_small_change_id(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": 1}, "id": "test_id"}
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
assert type == "autoupdate"
|
|
assert response.get("in_response") == "test_id"
|
|
assert response.get("content")["all_data"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_connect_up_to_date(communicator, set_config):
|
|
"""
|
|
Test, that a second request with change_id+1 from the first request does not
|
|
send anything, becuase the client is up to date.
|
|
"""
|
|
await set_config("general_system_enable_anonymous", True)
|
|
element_cache.cache_provider.change_id_data = {} # type: ignore
|
|
await communicator.connect()
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": 0}, "id": "test_id"}
|
|
)
|
|
response1 = await communicator.receive_json_from()
|
|
first_change_id = response1.get("content")["to_change_id"]
|
|
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "getElements",
|
|
"content": {"change_id": first_change_id + 1},
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
assert await communicator.receive_nothing()
|
|
|
|
|
|
@pytest.mark.xfail # This test is broken
|
|
@pytest.mark.asyncio
|
|
async def test_send_connect_twice_with_clear_change_id_cache_same_change_id_then_first_request(
|
|
communicator, set_config
|
|
):
|
|
"""
|
|
Test, that a second request with the change_id from the first request, returns
|
|
all data.
|
|
|
|
A client should not do this but request for change_id+1
|
|
"""
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await element_cache.cache_provider.clear_cache()
|
|
await communicator.connect()
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": 0}, "id": "test_id"}
|
|
)
|
|
response1 = await communicator.receive_json_from()
|
|
first_change_id = response1.get("content")["to_change_id"]
|
|
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "getElements",
|
|
"content": {"change_id": first_change_id},
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
response2 = await communicator.receive_json_from()
|
|
|
|
assert response2["type"] == "autoupdate"
|
|
assert response2.get("content")["all_data"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_changed_elements_no_douple_elements(communicator, set_config):
|
|
"""
|
|
Test, that when an elements is changed twice, it is only returned
|
|
onces when ask a range of change ids.
|
|
|
|
Test when all_data is false
|
|
"""
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
# Change element twice
|
|
await set_config("general_event_name", "Test Event")
|
|
await set_config("general_event_name", "Other value")
|
|
# Ask for all elements
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": 2}, "id": "test_id"}
|
|
)
|
|
|
|
response = await communicator.receive_json_from()
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert not response.get("content")["all_data"]
|
|
config_ids = [e["id"] for e in content["changed"]["core/config"]]
|
|
# test that config_ids are unique
|
|
assert len(config_ids) == len(set(config_ids))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_invalid_get_elements(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "getElements", "content": {"change_id": "some value"}, "id": "test_id"}
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
assert type == "error"
|
|
assert response.get("in_response") == "test_id"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_turn_on_autoupdate(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "autoupdate", "content": "on", "id": "test_id"}
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
# Change a config value
|
|
await set_config("general_event_name", "Test Event")
|
|
response = await communicator.receive_json_from()
|
|
|
|
id = config.get_key_to_id()["general_event_name"]
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "autoupdate"
|
|
assert content["changed"] == {
|
|
"core/config": [{"id": id, "key": "general_event_name", "value": "Test Event"}]
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_turn_off_autoupdate(get_communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
communicator = get_communicator("autoupdate=on")
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{"type": "autoupdate", "content": False, "id": "test_id"}
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
# Change a config value
|
|
await set_config("general_event_name", "Test Event")
|
|
assert await communicator.receive_nothing()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_listen_to_projector(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "listenToProjectors",
|
|
"content": {"projector_ids": [1]},
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "projector"
|
|
assert content == {
|
|
"1": [
|
|
{
|
|
"data": {"name": "slide1", "event_name": "OpenSlides"},
|
|
"element": {"id": 1, "name": "test/slide1"},
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_projector(communicator, set_config):
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "listenToProjectors",
|
|
"content": {"projector_ids": [1]},
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
await communicator.receive_json_from()
|
|
|
|
# Change a config value
|
|
await set_config("general_event_name", "Test Event")
|
|
response = await communicator.receive_json_from()
|
|
|
|
type = response.get("type")
|
|
content = response.get("content")
|
|
assert type == "projector"
|
|
assert content == {
|
|
"1": [
|
|
{
|
|
"data": {"name": "slide1", "event_name": "Test Event"},
|
|
"element": {"id": 1, "name": "test/slide1"},
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_projector_to_current_value(communicator, set_config):
|
|
"""
|
|
When a value does not change, the projector should not be updated.
|
|
"""
|
|
await set_config("general_system_enable_anonymous", True)
|
|
await communicator.connect()
|
|
await communicator.send_json_to(
|
|
{
|
|
"type": "listenToProjectors",
|
|
"content": {"projector_ids": [1]},
|
|
"id": "test_id",
|
|
}
|
|
)
|
|
await communicator.receive_json_from()
|
|
|
|
# Change a config value to current_value
|
|
await set_config("general_event_name", "OpenSlides")
|
|
|
|
assert await communicator.receive_nothing()
|