def test_message_nested_sync_input(client): data = {"dictionary": {"name": "test"}} message = { "actionQueue": [{ "payload": { "name": "dictionary.name", "value": "test1" }, "type": "syncInput", }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert not body["errors"] assert body["data"].get("dictionary") == {"name": "test1"}
def test_message_single(client, settings): _set_serial(settings, True, 5) data = {"counter": 0} component_id = shortuuid.uuid()[:8] message = { "actionQueue": [{ "payload": { "name": "slow_action" }, "type": "callMethod", }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": component_id, "epoch": time.time(), } response = client.post( "/message/tests.views.message.test_call_method_multiple.FakeSlowComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["data"].get("counter") == 1
def test_generate_checksum_str(settings): settings.SECRET_KEY = "asdf" expected = "TfxFqcQL" actual = generate_checksum('{"name": "test"}') assert expected == actual
def test_nested_setter(client): data = {"nested": {"check": False}} message = { "actionQueue": [ { "type": "callMethod", "payload": { "name": "nested.check=True" } }, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } body = _post_message_and_get_body(client, message) assert not body["errors"] assert body["data"]["nested"]["check"] == True
def test_equal_sign(client): data = {"nested": {"check": False}, "method_arg": ""} message = { "actionQueue": [ { "type": "callMethod", "payload": { "name": "test_method_string_arg('does=thiswork?')" }, }, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } body = _post_message_and_get_body(client, message) assert not body["errors"] assert body["data"]["method_arg"] == "does=thiswork?"
def test_message_call_method_refresh_redirect(client): data = {} message = { "actionQueue": [ {"payload": {"name": "test_refresh_redirect"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert "redirect" in body redirect = body["redirect"] assert redirect.get("url") == "/something-here" assert redirect.get("refresh") assert redirect.get("title") == "new title"
def test_message_two(client, settings): _set_serial(settings, True, 5) data = {"counter": 0} component_id = shortuuid.uuid()[:8] message = { "actionQueue": [{ "payload": { "name": "slow_action" }, "type": "callMethod", }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": component_id, } messages = [(client, 0, message), (client, 0.1, message)] with ThreadPool(len(messages)) as pool: results = pool.map(_message_runner, messages) assert len(results) == len(messages) first_result = results[0] first_body = orjson.loads(first_result.content) assert first_body["data"].get("counter") == 2 second_result = results[1] second_body = orjson.loads(second_result.content) assert second_body["queued"] == True
def test_message_call_method_reset(client): data = {"method_count": 1} message = { "actionQueue": [ {"payload": {"name": "method_count=2"}, "type": "callMethod"}, {"payload": {"name": "$reset"}, "type": "callMethod",}, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["data"]["method_count"] == 0 # `data` should contain all data (not just the diffs) for resets assert body["data"].get("check") is not None assert body["data"].get("dictionary") is not None
def test_message_call_method_poll_update(client): data = {} message = { "actionQueue": [ {"payload": {"name": "test_poll_update"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert "poll" in body poll = body["poll"] assert poll.get("timing") == 1000 assert poll.get("disable") == True assert poll.get("method") == "new_method"
def test_message_call_method_return_value(client): data = {} message = { "actionQueue": [ {"payload": {"name": "test_return_value"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert "return" in body return_data = body["return"] assert return_data.get("method") == "test_return_value" assert return_data.get("args") == [] assert return_data.get("kwargs") == {} assert return_data.get("value") == "booya"
def test_message_multiple_with_updated_data(client, settings): """ Not sure how likely this is to happen, but if the data got changed in a new queued request it gets disregarded because no sane way to merge it together. Not ideal, but not sure how to handle it. """ _set_serial(settings, True, 5) data = {"counter": 0} component_id = shortuuid.uuid()[:8] message = { "actionQueue": [{ "payload": { "name": "slow_action" }, "type": "callMethod", }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": component_id, } messages = [(client, 0, message), (client, 0.1, message), (client, 0.2, message)] # This new message with different data won't get used because not # sure how to reconcile this with resulting data from queued messages message_with_new_data = deepcopy(message) message_with_new_data["data"] = {"counter": 7} message_with_new_data["checksum"] = generate_checksum( orjson.dumps(message_with_new_data["data"])) messages.append((client, 0.4, message_with_new_data)) with ThreadPool(len(messages)) as pool: results = pool.map(_message_runner, messages) assert len(results) == len(messages) first_result = results[0] first_body = orjson.loads(first_result.content) assert first_body["data"].get("counter") == 4 for result in results[1:]: result = results[1] body = orjson.loads(result.content) assert body.get("queued") == True
def test_message_db_input_create(client): data = {"flavors": []} message = { "actionQueue": [ { "payload": { "model": "flavors", "db": { "pk": "", "name": "flavor" }, "fields": { "name": "Sugar Browning-Nutty" }, }, "type": "dbInput", }, { "type": "callMethod", "payload": { "name": "$refresh", "params": [] } }, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": "FDHcbzGf", } assert Flavor.objects.all().count() == 0 response = client.post( "/message/tests.views.fake_components.FakeModelComponent", message, content_type="application/json", ) flavor = Flavor.objects.get(id=1) assert flavor.name == "Sugar Browning-Nutty" body = orjson.loads(response.content) assert not body["errors"] assert body["data"] == { "flavors": [{ "pk": 1, "name": "Sugar Browning-Nutty", "decimal_value": None, "float_value": None, "label": "", "parent": None, }] }
def test_message_db_input_update(client): flavor = Flavor(id=1, name="Enzymatic-Flowery") flavor.save() data = {"flavors": [{"pk": flavor.pk, "title": flavor.name}]} message = { "actionQueue": [ { "payload": { "model": "flavors", "db": { "pk": flavor.pk, "name": "flavor" }, "fields": { "name": "Flowery-Floral" }, }, "type": "dbInput", }, { "type": "callMethod", "payload": { "name": "$refresh", "params": [] } }, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": "FDHcbzGf", } response = client.post( "/message/tests.views.fake_components.FakeModelComponent", message, content_type="application/json", ) flavor = Flavor.objects.get(id=1) assert flavor.name == "Flowery-Floral" body = orjson.loads(response.content) assert not body["errors"] assert body["data"] == { "flavors": [{ "pk": 1, "name": "Flowery-Floral", "decimal_value": None, "float_value": None, "label": "", "parent": None, }] }
def validate_checksum(self): """ Validates that the checksum in the request matches the data. Returns: Raises `AssertionError` if the checksums don't match. """ checksum = self.body.get("checksum") assert checksum, "Missing checksum" generated_checksum = generate_checksum( dumps(self.data, fix_floats=False)) assert checksum == generated_checksum, "Checksum does not match"
def test_message_nested_toggle(client): data = {"nested": {"check": False}} message = { "actionQueue": [ {"type": "callMethod", "payload": {"name": "$toggle('nested.check')"}}, ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": "FDHcbzGf", } body = _post_message_and_get_body(client, message) assert not body["errors"] assert body["data"]["nested"]["check"] == True
def post_and_get_response(client, url="", data={}, action_queue=[]): data = {} message = { "actionQueue": action_queue, "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( url, message, content_type="application/json", ) return response
def test_message_call_method(client): data = {} message = { "actionQueue": [{"payload": {"name": "test_method"}, "type": "callMethod",}], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["data"].get("method_count") == 1
def test_unicorn_render_hash(settings): settings.DEBUG = True token = Token( TokenType.TEXT, "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'", ) unicorn_node = unicorn(None, token) context = {} html = unicorn_node.render(context) assert '<script type="module"' in html assert len(re.findall('<script type="module"', html)) == 1 assert '"hash":"' in html # Assert that the content hash is correct script_idx = html.index("<script") rendered_content = html[:script_idx] expected_hash = generate_checksum(rendered_content) assert f'"hash":"{expected_hash}"' in html
def test_message_call_method_nested_toggle(client): data = {} message = { "actionQueue": [ {"payload": {"name": "$toggle('nested.check')"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": "FDHcbzGf", } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["data"].get("nested").get("check") == True
def test_message_call_method_no_validation(client): data = {} message = { "actionQueue": [ {"payload": {"name": "set_text_no_validation"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": "FDHcbzGf", } response = client.post( "/message/tests.views.fake_components.FakeValidationComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert not body["errors"]
def test_message_call_method_nested_setter(client): data = {"nested": {"check": True}} message = { "actionQueue": [ {"payload": {"name": "nested.check=False"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["data"].get("nested").get("check") == False
def test_message_call_method_hash_update(client): data = {} message = { "actionQueue": [ {"payload": {"name": "test_hash_update"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.fake_components.FakeComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert "redirect" in body assert body["redirect"].get("hash") == "#test=1"
def test_message_generated_checksum_matches_dom_checksum(client): data = {"clicked": False} message = { "actionQueue": [{ "payload": { "name": "test_method" }, "type": "callMethod", "target": None, }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.message.test_target.FakeTargetComponent", message, content_type="application/json", ) body = response.json() dom = body.get("dom") assert dom assert not body.get("partials") assert body.get("data", {}).get("clicked") == True soup = BeautifulSoup(dom, features="html.parser") for element in soup.find_all(): if "unicorn:checksum" in element.attrs: assert element.attrs["unicorn:checksum"] == body.get("checksum") break
def test_message_call_method_validation(client): data = {} message = { "actionQueue": [ {"payload": {"name": "set_text_with_validation"}, "type": "callMethod",} ], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], } response = client.post( "/message/tests.views.fake_components.FakeValidationComponent", message, content_type="application/json", ) body = orjson.loads(response.content) assert body["errors"] assert body["errors"]["number"] assert body["errors"]["number"][0]["code"] == "required" assert body["errors"]["number"][0]["message"] == "This field is required."
def test_message_second_request_not_queued_because_dummy_cache( client, settings): _set_serial(settings, True, 5, cache_backend="django.core.cache.backends.dummy.DummyCache") data = {"counter": 0} component_id = shortuuid.uuid()[:8] message = { "actionQueue": [{ "payload": { "name": "slow_action" }, "type": "callMethod", }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": component_id, } messages = [(client, 0, message), (client, 0.2, message)] with ThreadPool(len(messages)) as pool: results = pool.map(_message_runner, messages) assert len(results) == len(messages) first_result = results[0] first_body = orjson.loads(first_result.content) assert first_body["data"].get("counter") == 1 second_result = results[1] second_body = orjson.loads(second_result.content) assert second_body["data"].get("counter") == 1
def test_message_target_key(client): data = {"clicked": False} message = { "actionQueue": [{ "payload": { "name": "test_method" }, "type": "callMethod", "partial": { "target": "test-target-key" }, }], "data": data, "checksum": generate_checksum(orjson.dumps(data)), "id": shortuuid.uuid()[:8], "epoch": time.time(), } response = client.post( "/message/tests.views.message.test_target.FakeTargetComponent", message, content_type="application/json", ) body = response.json() assert body.get("dom") is None assert len(body["partials"]) == 1 assert body["partials"][0]["key"] == "test-target-key" assert body["partials"][0][ "dom"] == '<div unicorn:key="test-target-key"></div>' assert body.get("data", {}).get("clicked") == True
def _process_component_request( request: HttpRequest, component_request: ComponentRequest ) -> Dict: """ Process a `ComponentRequest`: 1. construct a Component view 2. set all of the properties on the view from the data 3. execute the type - update the properties based on the payload for "syncInput" - call the method specified for "callMethod" 4. validate any fields specified in a Django form 5. construct a `dict` that will get returned in a `JsonResponse` later on Args: param request: HttpRequest for the function-based view. param: component_request: Component request to process. Returns: `dict` with the following structure: { "id": component_id, "dom": html, // re-rendered version of the component after actions in the payload are completed "data": {}, // updated data after actions in the payload are completed "errors": {}, // form validation errors "return": {}, // optional return value from an executed action "parent": {}, // optional representation of the parent component } """ component = UnicornView.create( component_id=component_request.id, component_name=component_request.name, request=request, ) # Get a deepcopy of the data passed in to determine what fields are updated later original_data = copy.deepcopy(component_request.data) # Set component properties based on request data for (property_name, property_value) in component_request.data.items(): set_property_from_data(component, property_name, property_value) component.hydrate() validate_all_fields = False is_reset_called = False is_refresh_called = False return_data = None partials = [] for action in component_request.action_queue: if action.partial: partials.append(action.partial) else: partials = action.partials if action.action_type == "syncInput": sync_input.handle(component_request, component, action.payload) elif action.action_type == "callMethod": ( component, _is_refresh_called, _is_reset_called, _validate_all_fields, return_data, ) = call_method.handle(component_request, component, action.payload) is_refresh_called = is_refresh_called | _is_refresh_called is_reset_called = is_reset_called | _is_reset_called validate_all_fields = validate_all_fields | _validate_all_fields else: raise UnicornViewError(f"Unknown action_type '{action.action_type}'") component.complete() # Re-load frontend context variables to deal with non-serializable properties component_request.data = orjson.loads(component.get_frontend_context_variables()) # Get set of attributes that should be marked as `safe` safe_fields = [] if hasattr(component, "Meta") and hasattr(component.Meta, "safe"): if isinstance(component.Meta.safe, Sequence): for field_name in component.Meta.safe: if field_name in component._attributes().keys(): safe_fields.append(field_name) # Mark safe attributes as such before rendering for field_name in safe_fields: value = getattr(component, field_name) if isinstance(value, str): setattr(component, field_name, mark_safe(value)) # Send back all available data for reset or refresh actions updated_data = component_request.data if not is_reset_called: if not is_refresh_called: updated_data = {} for key, value in original_data.items(): if value != component_request.data.get(key): updated_data[key] = component_request.data.get(key) if validate_all_fields: component.validate() else: component.validate(model_names=list(updated_data.keys())) # Pass the current request so that it can be used inside the component template rendered_component = component.render(request=request) component.rendered(rendered_component) cache = caches[get_cache_alias()] try: cache.set(component.component_cache_key, get_cacheable_component(component)) except UnicornCacheError as e: logger.warning(e) partial_doms = [] if partials and all(partials): soup = BeautifulSoup(rendered_component, features="html.parser") for partial in partials: partial_found = False only_id = False only_key = False target = partial.get("target") if not target: target = partial.get("key") if target: only_key = True if not target: target = partial.get("id") if target: only_id = True assert target, "Partial target is required" if not only_id: for element in soup.find_all(): if ( "unicorn:key" in element.attrs and element.attrs["unicorn:key"] == target ): partial_doms.append({"key": target, "dom": str(element)}) partial_found = True break if not partial_found and not only_key: for element in soup.find_all(): if "id" in element.attrs and element.attrs["id"] == target: partial_doms.append({"id": target, "dom": str(element)}) partial_found = True break res = { "id": component_request.id, "data": updated_data, "errors": component.errors, "calls": component.calls, "checksum": generate_checksum(orjson.dumps(component_request.data)), } if partial_doms: res.update({"partials": partial_doms}) else: hash = generate_checksum(rendered_component) if ( component_request.hash == hash and (not return_data or not return_data.value) and not component.calls ): raise RenderNotModified() # Make sure that partials with comments or blank lines before the root element only return the root element soup = BeautifulSoup(rendered_component, features="html.parser") rendered_component = str(get_root_element(soup)) res.update( { "dom": rendered_component, "hash": hash, } ) if return_data: res.update( { "return": return_data.get_data(), } ) if return_data.redirect: res.update( { "redirect": return_data.redirect, } ) if return_data.poll: res.update( { "poll": return_data.poll, } ) parent_component = component.parent if parent_component: # TODO: Should parent_component.hydrate() be called? parent_frontend_context_variables = loads( parent_component.get_frontend_context_variables() ) parent_checksum = generate_checksum(dumps(parent_frontend_context_variables)) parent = { "id": parent_component.component_id, "checksum": parent_checksum, } if not partial_doms: parent_dom = parent_component.render() component.parent_rendered(parent_dom) try: cache.set( parent_component.component_cache_key, get_cacheable_component(parent_component), ) except UnicornCacheError as e: logger.warning(e) parent.update( { "dom": parent_dom, "data": parent_frontend_context_variables, "errors": parent_component.errors, } ) res.update({"parent": parent}) return res