def create_user( cls, client: FlaskClient, data: Optional[Dict[str, Any]] = None, roles: Optional[List[Union[str, Role]]] = None, ) -> Tuple[str, Dict[str, Any]]: assert Env.get_bool("MAIN_LOGIN_ENABLE") admin_headers, _ = cls.do_login(client, None, None) assert admin_headers is not None schema = cls.getDynamicInputSchema(client, "admin/users", admin_headers) user_data = cls.buildData(schema) if Connector.check_availability("smtp"): user_data["email_notification"] = False user_data["is_active"] = True user_data["expiration"] = None if roles: for idx, role in enumerate(roles): if isinstance(role, Role): roles[idx] = role.value user_data["roles"] = json.dumps(roles) if data: user_data.update(data) r = client.post(f"{API_URI}/admin/users", data=user_data, headers=admin_headers) assert r.status_code == 200 uuid = cls.get_content(r) return uuid, user_data
def test_destroy() -> None: # Only executed if tests are run with --destroy flag if os.getenv("TEST_DESTROY_MODE", "0") != "1": log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled") return # Always enable during core tests if not Connector.check_availability("authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return # if Connector.check_availability("sqlalchemy"): # sql = sqlalchemy.get_instance() # # Close previous connections, otherwise the new create_app will hang # sql.session.remove() # sql.session.close_all() auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None create_app(mode=ServerModes.DESTROY) try: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is None except ServiceUnavailable: pass
def test_pushpin(app: Flask) -> None: if not Connector.check_availability(CONNECTOR): try: obj = connector.get_instance() pytest.fail("No exception raised") # pragma: no cover except ServiceUnavailable: pass log.warning("Skipping {} tests: service not available", CONNECTOR) return None log.info("Executing {} tests", CONNECTOR) try: connector.get_instance(host="invalidhostname", port=123) pytest.fail( "No exception raised on unavailable service") # pragma: no cover except ServiceUnavailable: pass obj = connector.get_instance() assert obj is not None obj.disconnect() # a second disconnect should not raise any error obj.disconnect() # Create new connector with short expiration time obj = connector.get_instance(expiration=2, verification=1) obj_id = id(obj) # Connector is expected to be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) # The connection should have been checked and should be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) # Connection should have been expired and a new connector been created obj = connector.get_instance(expiration=2, verification=1) assert id(obj) != obj_id assert obj.is_connected() obj.disconnect() assert not obj.is_connected() # ... close connection again ... nothing should happens obj.disconnect() with connector.get_instance() as obj: assert obj is not None
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: neo4j_enabled = Connector.check_availability("neo4j") sqlalchemy_enabled = Connector.check_availability("sqlalchemy") if neo4j_enabled: from neomodel import db as neo4j_db if sqlalchemy_enabled: # thanks to connectors cache this should always match the # same instance that will be used from inside the endpoint from restapi.connectors import sqlalchemy alchemy_db = sqlalchemy.get_instance() try: if neo4j_enabled: neo4j_db.begin() # Transaction is already open... # if sqlalchemy_enabled: # pass out = func(self, *args, **kwargs) if neo4j_enabled: neo4j_db.commit() if sqlalchemy_enabled: alchemy_db.session.commit() return out except Exception as e: log.debug("Rolling backend database transaction") try: if neo4j_enabled: neo4j_db.rollback() if sqlalchemy_enabled: alchemy_db.session.rollback() except Exception as sub_ex: # pragma: no cover log.warning("Exception raised during rollback: {}", sub_ex) raise e
def send_notification( subject: str, template: str, # if None will be sent to the administrator to_address: Optional[str] = None, data: Optional[Dict[str, Any]] = None, user: Optional[User] = None, send_async: bool = False, ) -> bool: # Always enabled during tests if not Connector.check_availability("smtp"): # pragma: no cover return False title = get_project_configuration("project.title", default="Unkown title") reply_to = Env.get("SMTP_NOREPLY", Env.get("SMTP_ADMIN", "")) if data is None: data = {} data.setdefault("project", title) data.setdefault("reply_to", reply_to) if user: data.setdefault("username", user.email) data.setdefault("name", user.name) data.setdefault("surname", user.surname) html_body, plain_body = get_html_template(template, data) if not html_body: # pragma: no cover log.error("Can't load {}", template) return False subject = f"{title}: {subject}" if send_async: Mail.send_async( subject=subject, body=html_body, to_address=to_address, plain_body=plain_body, ) return False smtp_client = smtp.get_instance() return smtp_client.send( subject=subject, body=html_body, to_address=to_address, plain_body=plain_body, )
def get_instance(app: Flask) -> FlaskCache: # type: ignore # This check prevent KeyError raised during tests # Exactly as reported here: # https://github.com/sh4nks/flask-caching/issues/191 if not hasattr(mem, "cache"): cache_config = Cache.get_config( use_redis=Connector.check_availability("redis")) mem.cache = FlaskCache(config=cache_config) mem.cache.init_app(app) return mem.cache
def do_queries(self, value: str) -> None: neo4j_enabled = Connector.check_availability("neo4j") sql_enabled = Connector.check_availability("sqlalchemy") mysql_enabled = sql_enabled and sqlalchemy.SQLAlchemy.is_mysql() postgres_enabled = sql_enabled and not sqlalchemy.SQLAlchemy.is_mysql( ) # This is just a stub... to be completed if neo4j_enabled: graph = neo4j.get_instance() graph.cypher( "MATCH (g: Group) WHERE g.shortname = $value return g.shortname", value=value, ) graph.Group.nodes.get_or_none(shortname=value) elif postgres_enabled: sql = sqlalchemy.get_instance() t = sqlalchemy.text( 'SELECT * FROM "group" WHERE shortname = :value') sql.db.engine.execute(t, value=value) sql.Group.query.filter_by(shortname=value).first() elif mysql_enabled: sql = sqlalchemy.get_instance() t = sqlalchemy.text( "SELECT * FROM `group` WHERE shortname = :value") sql.db.engine.execute(t, value=value) sql.Group.query.filter_by(shortname=value).first()
def test_depends_on(self, client: FlaskClient) -> None: if Connector.check_availability("neo4j"): r = client.get(f"{API_URI}/tests/depends_on/neo4j") assert r.status_code == 200 r = client.get(f"{API_URI}/tests/depends_on_not/neo4j") assert r.status_code == 404 else: r = client.get(f"{API_URI}/tests/depends_on/neo4j") assert r.status_code == 404 r = client.get(f"{API_URI}/tests/depends_on_not/neo4j") assert r.status_code == 200
def test_destroy() -> None: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None create_app(mode=ServerModes.DESTROY) if Connector.check_availability("sqlalchemy"): with pytest.raises(ServiceUnavailable): auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) else: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is None
def test_ip_management(self) -> None: # Always enable during core tests if not Connector.check_availability( "authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() ip_data = auth.localize_ip("8.8.8.8") assert ip_data is not None # I don't know if this tests will be stable... assert ip_data == "United States" assert auth.localize_ip("8.8.8.8, 4.4.4.4") is None
def verify(services): """Verify connected service""" if len(services) == 0: log.warning("Empty list of services, nothing to be verified.") log.info("Provide list of services by using --services option") for service in services: if not Connector.check_availability(service): print_and_exit("Service {} not detected", service) log.info("Verifying service: {}", service) variables = Connector.services.get(service, {}) host, port = get_service_address(variables, "host", "port", service) wait_socket(host, port, service) log.info("Completed successfully")
def test_init() -> None: auth = Connector.get_authentication_instance() if Connector.authentication_service == "sqlalchemy": # Re-init does not work with MySQL due to issues with previous connections # Considering that: # 1) this is a workaround to test the initialization # (not the normal workflow used by the application) # 2) the init is already tested with any other DB, included postgres # 3) MySQL is not used by any project # => there is no need to go crazy in debugging this issue! if auth.db.is_mysql(): # type: ignore return # sql = sqlalchemy.get_instance() if Connector.check_availability("sqlalchemy"): # Prevents errors like: # sqlalchemy.exc.ResourceClosedError: This Connection is closed Connector.disconnect_all() # sql = sqlalchemy.get_instance() # # Close previous connections, otherwise the new create_app will hang # sql.session.remove() # sql.session.close_all() try: create_app(mode=ServerModes.INIT) # This is only a rough retry to prevent random errors from sqlalchemy except Exception: # pragma: no cover create_app(mode=ServerModes.INIT) auth = Connector.get_authentication_instance() try: user = auth.get_user(username=BaseAuthentication.default_user) # SqlAlchemy sometimes can raise an: # AttributeError: 'NoneType' object has no attribute 'twophase' # due to the multiple app created... should be an issue specific of this test # In that case... simply retry. except AttributeError: # pragma: no cover user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None
def verify(service: str) -> None: """Verify if a service is connected""" if not Connector.check_availability(service): print_and_exit("Service {} not detected", service) log.info("Verifying service: {}", service) variables = Connector.services.get(service, {}) host, port = get_service_address(variables, "host", "port", service) if host != "nohost": wait_socket(host, port, service) connector_module = Connector.get_module(service, BACKEND_PACKAGE) if not connector_module: # pragma: no cover print_and_exit("Connector {} not detected", service) c = connector_module.get_instance() log.info("{} successfully authenticated on {}", service, c.variables.get("host", service))
def mark_task_as_failed(self: Any, name: str, exception: Exception) -> NoReturn: if TESTING: self.request.id = "fixed-id" self.request.task = name task_id = self.request.id task_name = self.request.task arguments = str(self.request.args) # Removing username and password from urls in error stack clean_error_stack = "" for line in traceback.format_exc().split("\n"): clean_error_stack += f"{obfuscate_url(line)}\n" log.error("Celery task {} ({}) failed", task_id, task_name) log.error("Failed task arguments: {}", arguments[0:256]) log.error("Task error: {}", clean_error_stack) if Connector.check_availability("smtp"): log.info("Sending error report by email", task_id, task_name) send_celery_error_notification(task_id, task_name, arguments, clean_error_stack, -1) self.update_state( state=states.FAILURE, meta={ "exc_type": type(exception).__name__, "exc_message": traceback.format_exc().split("\n"), # 'custom': '...' }, ) self.send_event( "task-failed", # Retry sending the message if the connection is lost retry=True, exception=str(exception), traceback=traceback.format_exc(), ) raise Ignore(str(exception))
def test_init() -> None: # Only executed if tests are run with --destroy flag if os.getenv("TEST_DESTROY_MODE", "0") != "1": log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled") return # Always enable during core tests if not Connector.check_availability("authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() if Connector.authentication_service == "sqlalchemy": # Re-init does not work with MySQL due to issues with previous connections # Considering that: # 1) this is a workaround to test the initialization # (not the normal workflow used by the application) # 2) the init is already tested with any other DB, included postgres # 3) MySQL is not used by any project # => there is no need to go crazy in debugging this issue! if auth.db.is_mysql(): # type: ignore return # sql = sqlalchemy.get_instance() create_app(mode=ServerModes.INIT) auth = Connector.get_authentication_instance() try: user = auth.get_user(username=BaseAuthentication.default_user) # SqlAlchemy sometimes can raise an: # AttributeError: 'NoneType' object has no attribute 'twophase' # due to the multiple app created... should be an issue specific of this test # In that case... simply retry. except AttributeError: # pragma: no cover user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None
def mark_task_as_retriable(self: Any, name: str, exception: Exception, MAX_RETRIES: int) -> NoReturn: if TESTING: self.request.id = "fixed-id" self.request.task = name self.request.retries = 0 task_id = self.request.id task_name = self.request.task arguments = str(self.request.args) retry_num = 1 + self.request.retries # All retries attempts failed, # the error will be converted to permanent if retry_num > MAX_RETRIES: log.critical("MAX retries reached") mark_task_as_failed(self=self, name=name, exception=exception) # Removing username and password from urls in error stack clean_error_stack = "" for line in traceback.format_exc().split("\n"): clean_error_stack += f"{obfuscate_url(line)}\n" log.warning( "Celery task {} ({}) failed due to: {}, " "but will be retried (fail #{}/{})", task_id, task_name, exception, retry_num, MAX_RETRIES, ) if Connector.check_availability("smtp"): log.info("Sending error report by email", task_id, task_name) send_celery_error_notification(task_id, task_name, arguments, clean_error_stack, retry_num) raise exception
def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except BaseException: task_id = self.request.id task_name = self.request.task log.error("Celery task {} failed ({})", task_id, task_name) arguments = str(self.request.args) log.error("Failed task arguments: {}", arguments[0:256]) log.error("Task error: {}", traceback.format_exc()) if Connector.check_availability("smtp"): log.info("Sending error report by email", task_id, task_name) body = f""" Celery task {task_id} failed Name: {task_name} Arguments: {self.request.args} Error: {traceback.format_exc()} """ project = get_project_configuration( "project.title", default="Unkown title", ) subject = f"{project}: task {task_name} failed" from restapi.connectors import smtp smtp_client = smtp.get_instance() smtp_client.send(body, subject)
def admin_user_input(request: FlaskRequest, is_post: bool) -> Type[Schema]: is_admin = HTTPTokenAuth.is_session_user_admin(request, auth) attributes: MarshmallowSchema = {} if is_post: # This is because Email is not typed on marshmallow attributes["email"] = fields.Email( # type: ignore required=is_post, validate=validate.Length(max=100)) attributes["name"] = fields.Str( required=is_post, validate=validate.Length(min=1), metadata={"label": "First Name"}, ) attributes["surname"] = fields.Str( required=is_post, validate=validate.Length(min=1), metadata={"label": "Last Name"}, ) attributes["password"] = fields.Str( required=is_post, validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH), metadata={"password": True}, ) if Connector.check_availability("smtp"): attributes["email_notification"] = fields.Bool( metadata={"label": "Notify password by email"}) attributes["is_active"] = fields.Bool( dump_default=True, required=False, metadata={"label": "Activate user"}, ) roles = {r.name: r.description for r in auth.get_roles()} if not is_admin and RoleEnum.ADMIN.value in roles: roles.pop(RoleEnum.ADMIN.value) attributes["roles"] = fields.List( fields.Str(validate=validate.OneOf( choices=[r for r in roles.keys()], labels=[r for r in roles.values()], )), dump_default=[auth.default_role], required=False, unique=True, metadata={ "label": "Roles", "description": "", "extra_descriptions": auth.role_descriptions, }, ) group_keys = [] group_labels = [] for g in auth.get_groups(): group_keys.append(g.uuid) group_labels.append(f"{g.shortname} - {g.fullname}") if len(group_keys) == 1: default_group = group_keys[0] else: default_group = None attributes["group"] = fields.Str( required=is_post, dump_default=default_group, validate=validate.OneOf(choices=group_keys, labels=group_labels), metadata={ "label": "Group", "description": "The group to which the user belongs", }, ) attributes["expiration"] = fields.DateTime( required=False, allow_none=True, metadata={ "label": "Account expiration", "description": "This user will be blocked after this date", }, ) if custom_fields := mem.customizer.get_custom_input_fields( request=request, scope=mem.customizer.ADMIN): attributes.update(custom_fields)
import pytest import pytz from faker import Faker from flask import Flask from neo4j.exceptions import CypherSyntaxError from restapi.connectors import Connector from restapi.connectors import neo4j as connector from restapi.connectors.neo4j.parser import DataDump, NodeDump, RelationDump from restapi.exceptions import ServiceUnavailable from restapi.services.authentication import BaseAuthentication from restapi.tests import API_URI, BaseTests, FlaskClient from restapi.utilities.logs import log CONNECTOR = "neo4j" CONNECTOR_AVAILABLE = Connector.check_availability(CONNECTOR) @pytest.mark.skipif(CONNECTOR_AVAILABLE, reason=f"This test needs {CONNECTOR} to be not available") def test_no_neo4j() -> None: with pytest.raises(ServiceUnavailable): connector.get_instance() log.warning("Skipping {} tests: service not available", CONNECTOR) return None @pytest.mark.skipif(not CONNECTOR_AVAILABLE, reason=f"This test needs {CONNECTOR} to be available")
def verify_token_is_not_valid(auth: BaseAuthentication, token: str, ttype: Optional[str] = None) -> None: unpacked_token = auth.verify_token(token, token_type=ttype) assert not unpacked_token[0] assert unpacked_token[1] is None assert unpacked_token[2] is None assert unpacked_token[3] is None with pytest.raises(Exception): auth.verify_token(token, token_type=ttype, raiseErrors=True) @pytest.mark.skipif( not Connector.check_availability("authentication"), reason="This test needs authentication to be available", ) class TestApp(BaseTests): def test_password_management(self, faker: Faker) -> None: # Ensure name and surname longer than 3 name = self.get_first_name(faker) surname = self.get_last_name(faker) # Ensure an email not containing name and surname email = self.get_random_email(faker, name, surname) auth = Connector.get_authentication_instance() min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999)
def getInputSchema(request, is_post): # as defined in Marshmallow.schema.from_dict attributes: Dict[str, Union[fields.Field, type]] = {} if is_post: attributes["email"] = fields.Email(required=is_post) attributes["name"] = fields.Str(required=is_post, validate=validate.Length(min=1)) attributes["surname"] = fields.Str(required=is_post, validate=validate.Length(min=1)) attributes["password"] = fields.Str( required=is_post, password=True, validate=validate.Length(min=auth.MIN_PASSWORD_LENGTH), ) if Connector.check_availability("smtp"): attributes["email_notification"] = fields.Bool( label="Notify password by email") attributes["is_active"] = fields.Bool(label="Activate user", default=True, required=False) roles = {r.name: r.description for r in auth.get_roles()} attributes["roles"] = AdvancedList( fields.Str(validate=validate.OneOf( choices=[r for r in roles.keys()], labels=[r for r in roles.values()], )), required=False, label="Roles", description="", unique=True, multiple=True, ) group_keys = [] group_labels = [] for g in auth.get_groups(): group_keys.append(g.uuid) group_labels.append(f"{g.shortname} - {g.fullname}") if len(group_keys) == 1: default_group = group_keys[0] else: default_group = None attributes["group"] = fields.Str( label="Group", description="The group to which the user belongs", required=is_post, default=default_group, validate=validate.OneOf(choices=group_keys, labels=group_labels), ) attributes["expiration"] = fields.DateTime( required=False, allow_none=True, label="Account expiration", description="This user will be blocked after this date", ) if custom_fields := mem.customizer.get_custom_input_fields( request=request, scope=mem.customizer.ADMIN): attributes.update(custom_fields)
class TestApp(BaseTests): def test_inputs(self, client: FlaskClient) -> None: # This test verifies that buildData is always able to randomly create # valid inputs for endpoints with inputs defined by marshamallow schemas schema = self.get_dynamic_input_schema(client, "tests/inputs", {}) # Expected number of fields assert len(schema) == 14 for field in schema: # Always in the schema assert "key" in field assert "type" in field assert "label" in field assert "description" in field assert "required" in field # Other optional keys # - default # - min # - max # - options # - schema in case of nested fields field = schema[0] assert len(field) == 6 # 5 mandatory fields + min assert field["key"] == "mystr" assert field["type"] == "string" # This is the default case: both label and description are not explicitly set # if key is lower-cased the corrisponding label will be titled assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "min" in field assert field["min"] == 4 assert "max" not in field field = schema[1] assert len(field) == 5 # 5 mandatory fields, min and max not set assert field["key"] == "MYDATE" assert field["type"] == "date" # Here the key is not lower cased and the label is not explicitly set # So the label will exactly match the key (without additiona of .title) assert field["label"] == field["key"] assert field["label"] != field["key"].title() assert field["description"] == field["label"] assert field["required"] field = schema[2] assert len(field) == 7 # 5 mandatory fields + min + max assert field["key"] == "MYDATETIME" assert field["type"] == "datetime" # Here the key is not lower cased and the label is not explicitly set # So the label will exactly match the key (without additiona of .title) assert field["label"] == field["key"] assert field["label"] != field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "min" in field assert "max" in field field = schema[3] assert len(field) == 7 # 5 mandatory fields + min + max assert field["key"] == "myint_exclusive" assert field["type"] == "int" # Here an explicit label is defined but not a description, so is == to the label assert field["label"] != field["key"] assert field["label"] != field["key"].title() assert field["label"] == "Int exclusive field" assert field["description"] == field["label"] assert field["required"] assert "min" in field assert field["min"] == 2 assert "max" in field assert field["max"] == 9 field = schema[4] assert len(field) == 7 # 5 mandatory fields + min + max assert field["key"] == "myint_inclusive" assert field["type"] == "int" # Here both label and description are explicitly set assert field["label"] != field["key"] assert field["label"] != field["key"].title() assert field["label"] == "Int inclusive field" assert field["description"] != field["label"] assert field["description"] == "This field accepts values in a defined range" assert field["required"] assert "min" in field assert field["min"] == 1 assert "max" in field assert field["max"] == 10 field = schema[5] assert len(field) == 6 # 5 mandatory fields + options assert field["key"] == "myselect" assert field["type"] == "string" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "options" in field assert isinstance(field["options"], dict) assert len(field["options"]) == 2 assert "a" in field["options"] assert "b" in field["options"] # The field defines labels and keys for all options assert field["options"]["a"] == "A" assert field["options"]["b"] == "B" field = schema[6] assert len(field) == 6 # 5 mandatory fields + options assert field["key"] == "myselect2" assert field["type"] == "string" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "options" in field assert isinstance(field["options"], dict) assert len(field["options"]) == 2 assert "a" in field["options"] assert "b" in field["options"] # The field wrongly defines labels, so are defaulted to keys assert field["options"]["a"] == "a" assert field["options"]["b"] == "b" field = schema[7] assert len(field) == 6 # 5 mandatory fields + max assert field["key"] == "mymaxstr" assert field["type"] == "string" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "min" not in field assert "max" in field assert field["max"] == 7 field = schema[8] assert len(field) == 7 # 5 mandatory fields + min + max assert field["key"] == "myequalstr" assert field["type"] == "string" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "min" in field assert "max" in field assert field["min"] == 6 assert field["max"] == 6 field = schema[9] assert len(field) == 6 # 5 mandatory fields + schema assert field["key"] == "mynested" assert field["type"] == "nested" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "schema" in field field = schema[10] assert len(field) == 6 # 5 mandatory fields + schema assert field["key"] == "mynullablenested" assert field["type"] == "nested" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] assert "schema" in field field = schema[11] assert len(field) == 5 # 5 mandatory fields assert field["key"] == "mylist" assert field["type"] == "string[]" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] field = schema[12] assert len(field) == 5 # 5 mandatory fields assert field["key"] == "mylist2" # The list is defined as List(CustomInt) and CustomInt is resolved as int assert field["type"] == "int[]" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] field = schema[13] assert len(field) == 5 # 5 mandatory fields assert field["key"] == "mylist3" # The type is key[] ... should be something more explicative like FieldName[] # assert field["type"] == "CustomGenericField[]" assert field["type"] == "mylist3[]" assert field["label"] == field["key"].title() assert field["description"] == field["label"] assert field["required"] data = self.buildData(schema) # mylist3 is a list of custom field, buildData can't automatically set a value assert "mylist3" not in data data["mylist3"] = orjson.dumps(["mycustominputvalue"]).decode("UTF8") r = client.post(f"{API_URI}/tests/inputs", json=data) assert r.status_code == 204 # This is to verify that access_token, if provided is excluded from parameters # And do not raise any ValidationError for unknown input if Env.get_bool("AUTH_ENABLE"): _, token = self.do_login(client, None, None) data["access_token"] = token r = client.post(f"{API_URI}/tests/inputs", json=data) assert r.status_code == 204 # This is to verify that unknown inputs raise a ValidationError data["unknown"] = "input" r = client.post(f"{API_URI}/tests/inputs", json=data) assert r.status_code == 400 @pytest.mark.skipif( not Connector.check_availability("neo4j"), reason="This test needs neo4j to be available", ) def test_neo4j_inputs(self, client: FlaskClient) -> None: headers, _ = self.do_login(client, None, None) schema = self.get_dynamic_input_schema(client, "tests/neo4jinputs", headers) assert len(schema) == 1 field = schema[0] assert field["key"] == "choice" # This is because the Neo4jChoice field is not completed for deserialization # It is should be automatically translated into a select, with options by # including a validation OneOf assert "options" not in field r = client.post( f"{API_URI}/tests/neo4jinputs", json={"choice": "A"}, headers=headers ) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "choice" in response assert "key" in response["choice"] assert "description" in response["choice"] assert response["choice"]["key"] == "A" assert response["choice"]["description"] == "AAA" assert "relationship_count" in response assert isinstance(response["relationship_count"], int) assert response["relationship_count"] > 0 assert "relationship_single" in response assert isinstance(response["relationship_single"], dict) assert "uuid" in response["relationship_single"] assert "relationship_many" in response assert isinstance(response["relationship_many"], list) assert len(response["relationship_many"]) > 0 assert isinstance(response["relationship_many"][0], dict) assert "token_type" in response["relationship_many"][0] r = client.post( f"{API_URI}/tests/neo4jinputs", json={"choice": "B"}, headers=headers ) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "choice" in response assert "key" in response["choice"] assert "description" in response["choice"] assert response["choice"]["key"] == "B" assert response["choice"]["description"] == "BBB" r = client.post( f"{API_URI}/tests/neo4jinputs", json={"choice": "C"}, headers=headers ) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "choice" in response assert "key" in response["choice"] assert "description" in response["choice"] assert response["choice"]["key"] == "C" assert response["choice"]["description"] == "CCC" r = client.post( f"{API_URI}/tests/neo4jinputs", json={"choice": "D"}, headers=headers ) # This should fail, but Neo4jChoice are not validated as input # assert r.status_code == 400 # Since validation is not implemented, D is accepted But since it is # not included in the choice, the description will simply match the key assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "choice" in response assert "key" in response["choice"] assert "description" in response["choice"] assert response["choice"]["key"] == "D" assert response["choice"]["description"] == "D"
from restapi import decorators from restapi.config import TESTING from restapi.connectors import Connector, neo4j from restapi.exceptions import BadRequest from restapi.models import Neo4jChoice, Neo4jSchema, Schema, fields from restapi.rest.definition import EndpointResource, Response from restapi.utilities.logs import log if TESTING and Connector.check_availability("neo4j"): from restapi.connectors.neo4j.models import Group, User CHOICES_tuple = (("A", "A"), ("B", "B"), ("C", "C")) CHOICES_dict = {"A": "A", "B": "B", "C": "C"} class Output(Schema): val = fields.Integer() created = fields.DateTime() modified1 = fields.DateTime() modified2 = fields.DateTime() user = Neo4jSchema( User, fields=( "uuid", "email", "name", "surname", "is_active", "last_password_change", ), )
def test_sqlalchemy(app: Flask) -> None: if not Connector.check_availability(CONNECTOR): try: obj = connector.get_instance() pytest.fail("No exception raised") # pragma: no cover except ServiceUnavailable: pass log.warning("Skipping {} tests: service not available", CONNECTOR) return None log.info("Executing {} tests", CONNECTOR) if not connector.SQLAlchemy.is_mysql(): try: connector.get_instance(host="invalidhostname", port=123) pytest.fail("No exception raised on unavailable service" ) # pragma: no cover except ServiceUnavailable: pass try: connector.get_instance(user="******") pytest.fail( "No exception raised on unavailable service") # pragma: no cover except ServiceUnavailable: pass obj = connector.get_instance() assert obj is not None try: obj.InvalidModel pytest.fail("No exception raised on InvalidModel") # pragma: no cover except AttributeError as e: assert str(e) == "Model InvalidModel not found" obj.disconnect() # a second disconnect should not raise any error obj.disconnect() # Create new connector with short expiration time obj = connector.get_instance(expiration=2, verification=1) obj_id = id(obj) obj_db_id = id(obj.db) # Connector is expected to be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) # The connection should have been checked and should be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) obj = connector.get_instance(expiration=2, verification=1) # With alchemy the connection object remains the same... assert id(obj) != obj_id assert id(obj.db) == obj_db_id assert obj.is_connected() obj.disconnect() assert not obj.is_connected() # ... close connection again ... nothing should happens obj.disconnect()
def test_admin(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping admin authorizations tests") return # List of all paths to be tested. After each test a path will be removed. # At the end the list is expected to be empty paths = self.get_paths(client) uuid, data = self.create_user(client, roles=[Role.ADMIN]) headers, _ = self.do_login(client, data.get("email"), data.get("password")) # These are public paths = self.check_endpoint(client, "GET", "/api/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/specs", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/login", headers, True, paths) if Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS") > 0: paths = self.check_endpoint(client, "POST", "/auth/login/unlock/<token>", headers, True, paths) if Env.get_bool("ALLOW_REGISTRATION"): paths = self.check_endpoint(client, "POST", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/profile/activate", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile/activate/<token>", headers, True, paths) if Env.get_bool("ALLOW_PASSWORD_RESET" ) and Connector.check_availability("smtp"): paths = self.check_endpoint(client, "POST", "/auth/reset", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/reset/<token>", headers, True, paths) # These are allowed to each user paths = self.check_endpoint(client, "GET", "/auth/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PATCH", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/tokens", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/auth/tokens/<token>", headers, True, paths) # These are allowed to coordinators paths = self.check_endpoint(client, "GET", "/api/group/users", headers, False, paths) # These are allowed to staff # ... none # These are allowed to admins paths = self.check_endpoint(client, "GET", "/api/admin/users", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/users", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/users/<user_id>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/logins", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/tokens", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/tokens/<token>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/stats", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/mail", headers, True, paths) # logout MUST be the last one or the token will be invalidated!! :-) paths = self.check_endpoint(client, "GET", "/auth/logout", headers, True, paths) assert paths == [] self.delete_user(client, uuid)
import pytest import pytz from faker import Faker from flask import Flask from neo4j.exceptions import CypherSyntaxError from restapi.connectors import Connector from restapi.connectors import neo4j as connector from restapi.env import Env from restapi.exceptions import ServiceUnavailable from restapi.tests import API_URI, BaseTests, FlaskClient from restapi.utilities.logs import log CONNECTOR = "neo4j" if not Connector.check_availability(CONNECTOR): try: obj = connector.get_instance() pytest.fail("No exception raised") # pragma: no cover except ServiceUnavailable: pass log.warning("Skipping {} tests: service not available", CONNECTOR) # Alwas enabled during core tests elif not Env.get_bool("TEST_CORE_ENABLED"): # pragma: no cover log.warning("Skipping {} tests: only avaiable on core", CONNECTOR) else: log.info("Executing {} tests", CONNECTOR)
def test_celery(app: Flask, faker: Faker) -> None: if not Connector.check_availability(CONNECTOR): try: obj = connector.get_instance() pytest.fail("No exception raised") # pragma: no cover except ServiceUnavailable: pass log.warning("Skipping {} tests: service not available", CONNECTOR) return None log.info("Executing {} tests", CONNECTOR) obj = connector.get_instance() assert obj is not None task = obj.celery_app.send_task("test_task") assert task is not None assert task.id is not None if obj.variables.get("backend") == "RABBIT": log.warning( "Due to limitations on RABBIT backend task results will not be tested" ) else: try: r = task.get(timeout=10) assert r is not None # This is the task output, as defined in task_template.py.j2 assert r == "Task executed!" assert task.status == "SUCCESS" assert task.result == "Task executed!" except celery.exceptions.TimeoutError: # pragma: no cover pytest.fail( f"Task timeout, result={task.result}, status={task.status}") if CeleryExt.CELERYBEAT_SCHEDULER is None: try: obj.get_periodic_task("does_not_exist") pytest.fail("get_periodic_task with unknown CELERYBEAT_SCHEDULER" ) # pragma: no cover except AttributeError as e: assert str(e) == "Unsupported celery-beat scheduler: None" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: obj.delete_periodic_task("does_not_exist") pytest.fail( "delete_periodic_task with unknown CELERYBEAT_SCHEDULER" ) # pragma: no cover except AttributeError as e: assert str(e) == "Unsupported celery-beat scheduler: None" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: obj.create_periodic_task(name="task1", task="task.does.not.exists", every="60") pytest.fail( "create_periodic_task with unknown CELERYBEAT_SCHEDULER" ) # pragma: no cover except AttributeError as e: assert str(e) == "Unsupported celery-beat scheduler: None" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: obj.create_crontab_task(name="task2", task="task.does.not.exists", minute="0", hour="1") pytest.fail("create_crontab_task with unknown CELERYBEAT_SCHEDULER" ) # pragma: no cover except AttributeError as e: assert str(e) == "Unsupported celery-beat scheduler: None" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") else: assert obj.get_periodic_task("does_not_exist") is None assert not obj.delete_periodic_task("does_not_exist") obj.create_periodic_task(name="task1", task="task.does.not.exists", every="60") assert obj.delete_periodic_task("task1") assert not obj.delete_periodic_task("task1") obj.create_periodic_task( name="task1_bis", task="task.does.not.exists", every="60", period="seconds", args=["a", "b", "c"], kwargs={ "a": 1, "b": 2, "c": 3 }, ) assert obj.delete_periodic_task("task1_bis") assert not obj.delete_periodic_task("task1_bis") # cron at 01:00 obj.create_crontab_task(name="task2", task="task.does.not.exists", minute="0", hour="1") assert obj.delete_periodic_task("task2") assert not obj.delete_periodic_task("task2") obj.create_crontab_task( name="task2_bis", task="task.does.not.exists", minute="0", hour="1", day_of_week="*", day_of_month="*", month_of_year="*", args=["a", "b", "c"], kwargs={ "a": 1, "b": 2, "c": 3 }, ) assert obj.delete_periodic_task("task2_bis") assert not obj.delete_periodic_task("task2_bis") if CeleryExt.CELERYBEAT_SCHEDULER == "REDIS": obj.create_periodic_task( name="task3", task="task.does.not.exists", every=60, ) assert obj.delete_periodic_task("task3") obj.create_periodic_task(name="task4", task="task.does.not.exists", every=60, period="seconds") assert obj.delete_periodic_task("task4") obj.create_periodic_task(name="task5", task="task.does.not.exists", every=60, period="minutes") assert obj.delete_periodic_task("task5") obj.create_periodic_task(name="task6", task="task.does.not.exists", every=60, period="hours") assert obj.delete_periodic_task("task6") obj.create_periodic_task(name="task7", task="task.does.not.exists", every=60, period="days") assert obj.delete_periodic_task("task7") try: obj.create_periodic_task( name="task8", task="task.does.not.exists", every="60", period="years", # type: ignore ) except BadRequest as e: assert str(e) == "Invalid timedelta period: years" obj.create_periodic_task( name="task9", task="task.does.not.exists", every=timedelta(seconds=60), ) assert obj.delete_periodic_task("task9") try: obj.create_periodic_task( name="task10", task="task.does.not.exists", every=["60"], # type: ignore ) except AttributeError as e: assert str( e) == "Invalid input parameter every = ['60'] (type list)" try: obj.create_periodic_task( name="task11", task="task.does.not.exists", every="invalid", ) except AttributeError as e: assert str( e) == "Invalid input parameter every = invalid (type str)" else: obj.create_periodic_task(name="task3", task="task.does.not.exists", every="60", period="minutes") assert obj.delete_periodic_task("task3") obj.disconnect() # a second disconnect should not raise any error obj.disconnect() # Create new connector with short expiration time obj = connector.get_instance(expiration=2, verification=1) obj_id = id(obj) # Connector is expected to be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) # The connection should have been checked and should be still valid obj = connector.get_instance(expiration=2, verification=1) assert id(obj) == obj_id time.sleep(1) # Connection should have been expired and a new connector been created obj = connector.get_instance(expiration=2, verification=1) assert id(obj) != obj_id assert obj.is_connected() obj.disconnect() assert not obj.is_connected() # ... close connection again ... nothing should happens obj.disconnect() with connector.get_instance() as obj: assert obj is not None app = create_app(mode=ServerModes.WORKER) assert app is not None from restapi.utilities.logs import LOGS_FILE assert os.environ["HOSTNAME"] == "backend-server" assert LOGS_FILE == "backend-server" # this decorator is expected to be used in celery context, i.e. the self reference # should contains a request, injected by celery. Let's mock this by injecting an # artificial self @send_errors_by_email def this_function_raises_exceptions(self): raise AttributeError("Just an exception") class FakeRequest: def __init__(self, task_id, task, args): self.id = task_id self.task = task self.args = args class FakeSelf: def __init__(self, task_id, task, args): self.request = FakeRequest(task_id, task, args) task_id = faker.pystr() task_name = faker.pystr() task_args = [faker.pystr()] this_function_raises_exceptions(FakeSelf(task_id, task_name, task_args)) mail = BaseTests.read_mock_email() assert mail.get("body") is not None assert f"Celery task {task_id} failed" in mail.get("body") assert f"Name: {task_name}" in mail.get("body") assert f"Arguments: {str(task_args)}" in mail.get("body") assert "Error: Traceback (most recent call last):" in mail.get("body") assert 'raise AttributeError("Just an exception")' in mail.get("body")
def do_login( cls, client: FlaskClient, USER: Optional[str], PWD: Optional[str], status_code: int = 200, data: Optional[Dict[str, Any]] = None, test_failures: bool = False, ) -> Tuple[Optional[Dict[str, str]], str]: """ Make login and return both token and authorization header """ if not Connector.check_availability( "authentication"): # pragma: no cover pytest.fail("Authentication is not enabled") if USER is None or PWD is None: BaseAuthentication.load_default_user() BaseAuthentication.load_roles() if USER is None: USER = BaseAuthentication.default_user if PWD is None: PWD = BaseAuthentication.default_password assert USER is not None assert PWD is not None if data is None: data = {} data["username"] = USER data["password"] = PWD r = client.post(f"{AUTH_URI}/login", json=data) content = orjson.loads(r.data.decode("utf-8")) if r.status_code == 403: # This 403 is expected, return an invalid value or you can enter a loop! if status_code == 403: return None, content if isinstance(content, dict) and content.get("actions"): actions = content.get("actions", []) for action in actions: if action == "TOTP": continue if action == "FIRST LOGIN": continue if action == "PASSWORD EXPIRED": continue data = {} if "FIRST LOGIN" in actions or "PASSWORD EXPIRED" in actions: events = cls.get_last_events(1) assert events[0].event == Events.password_expired.value # assert events[0].user == USER newpwd = cls.faker.password(strong=True) if test_failures: data["new_password"] = newpwd data["password_confirm"] = cls.faker.password( strong=True) if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp(USER) BaseTests.do_login( client, USER, PWD, data=data, status_code=409, ) # Test failure of password change if TOTP is wrong or missing if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["new_password"] = newpwd data["password_confirm"] = newpwd data.pop("totp_code", None) BaseTests.do_login( client, USER, PWD, data=data, status_code=403, ) data["new_password"] = newpwd data["password_confirm"] = newpwd # random int with 6 digits data["totp_code"] = str( cls.faker.pyint(min_value=100000, max_value=999999)) BaseTests.do_login( client, USER, PWD, data=data, status_code=401, ) # Change the password to silence FIRST_LOGIN and PASSWORD_EXPIRED data["new_password"] = newpwd data["password_confirm"] = newpwd if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp(USER) BaseTests.do_login( client, USER, PWD, data=data, ) # Change again to restore the default password # and keep all other tests fully working data["new_password"] = PWD data["password_confirm"] = PWD if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp(USER) return BaseTests.do_login( client, USER, newpwd, data=data, ) # in this case FIRST LOGIN has not been executed # => login by sending the TOTP code if "TOTP" in actions: # Only directly tested => no coverage if test_failures: # pragma: no cover # random int with 6 digits data["totp_code"] = cls.faker.pyint(min_value=100000, max_value=999999) BaseTests.do_login( client, USER, PWD, data=data, status_code=401, ) data["totp_code"] = BaseTests.generate_totp(USER) return BaseTests.do_login( client, USER, PWD, data=data, ) # FOR DEBUGGING WHEN ADVANCED AUTH OPTIONS ARE ON # if r.status_code != 200: # c = orjson.loads(r.data.decode("utf-8")) # log.error(c) assert r.status_code == status_code # when 200 OK content is the token assert content is not None return {"Authorization": f"Bearer {content}"}, content
from restapi import decorators from restapi.config import get_project_configuration from restapi.connectors import Connector, smtp from restapi.endpoints.profile_activation import send_activation_link from restapi.env import Env from restapi.exceptions import Conflict, ServiceUnavailable from restapi.models import Schema, fields, validate from restapi.rest.definition import EndpointResource, Response from restapi.services.authentication import DEFAULT_GROUP_NAME from restapi.utilities.globals import mem # from restapi.utilities.logs import log # This endpoint requires the server to send the activation token via email if Connector.check_availability("smtp"): auth = Connector.get_authentication_instance() # Note that these are callables returning a model, not models! # They will be executed a runtime def getInputSchema(request): # as defined in Marshmallow.schema.from_dict attributes: Dict[str, Union[fields.Field, type]] = {} attributes["name"] = fields.Str(required=True) attributes["surname"] = fields.Str(required=True) attributes["email"] = fields.Email(required=True, label="Username (email address)") attributes["password"] = fields.Str(
def test_websockets(self, client: FlaskClient, faker: Faker) -> None: if not Connector.check_availability("pushpin"): log.warning("Skipping websockets test: pushpin service not available") return log.info("Executing websockets tests") channel = faker.pystr() r = client.post(f"{API_URI}/socket/{channel}") assert r.status_code == 401 r = client.put(f"{API_URI}/socket/{channel}/1") assert r.status_code == 401 headers, _ = self.do_login(client, None, None) assert headers is not None headers["Content-Type"] = "application/websocket-events" r = client.post(f"{API_URI}/socket/{channel}", headers=headers) assert r.status_code == 400 error = "Cannot decode websocket request: invalid in_event" assert self.get_content(r) == error data = b"\r\n" r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers) assert r.status_code == 400 error = "Cannot understand websocket request" assert self.get_content(r) == error data = b"OPEN" r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers) assert r.status_code == 400 error = "Cannot decode websocket request: invalid format" assert self.get_content(r) == error data = b"XYZ\r\n" r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers) assert r.status_code == 400 error = "Cannot understand websocket request" assert self.get_content(r) == error data = b"OPEN\r\n" r = client.post(f"{API_URI}/socket/{channel}", data=data, headers=headers) assert r.status_code == 200 content = r.data.decode("utf-8").split("\n") assert len(content) >= 3 assert content[0] == "OPEN\r" assert content[1] == "TEXT 3a\r" assert content[2] == 'c:{"channel": "%s", "type": "subscribe"}\r' % channel assert "Sec-WebSocket-Extensions" in r.headers assert r.headers.get("Sec-WebSocket-Extensions") == "grip" r = client.put(f"{API_URI}/socket/{channel}/1", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "Message received: True (sync=True)" r = client.put(f"{API_URI}/socket/{channel}/0", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "Message received: True (sync=False)" # send message on a different channel channel = faker.pystr() r = client.put(f"{API_URI}/socket/{channel}/1", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "Message received: True (sync=True)" r = client.post(f"{API_URI}/stream/{channel}", headers=headers) assert r.status_code == 200 content = r.data.decode("utf-8") assert content == "Stream opened, prepare yourself!\n" assert "Grip-Hold" in r.headers assert r.headers["Grip-Hold"] == "stream" assert "Grip-Channel" in r.headers r = client.put(f"{API_URI}/stream/{channel}/1", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "Message received: True (sync=True)" r = client.put(f"{API_URI}/stream/{channel}/0", headers=headers) assert r.status_code == 200 assert self.get_content(r) == "Message received: True (sync=False)"