def test_vulnerabilities(client: FlaskClient) -> None: strings = ( "xx", "x'x", 'x"x', "x`x", "x#x", "x--x", "x\\*x", "x*x", "x+x", "x;x", "x(x", "x)x", ) for s in strings: r = client.get(f"{API_URI}/tests/vulnerabilities/{s}", query_string={"payload": s}) assert r.status_code == 200 r = client.post(f"{API_URI}/tests/vulnerabilities/{s}", json={"payload": s}) assert r.status_code == 200 # Can't test x//x as url parameter r = client.get(f"{API_URI}/tests/vulnerabilities/x", query_string={"payload": "x//x"}) assert r.status_code == 200 r = client.post(f"{API_URI}/tests/vulnerabilities/x", json={"payload": "x//x"}) assert r.status_code == 200
def test_test_unused_credentials(self, client: FlaskClient, faker: Faker) -> None: assert BaseTests.unused_credentials is not None assert len(BaseTests.unused_credentials) == 3 data = { "username": BaseTests.unused_credentials[0], "password": faker.password(strong=True), } # Credentials are verified before the inactivity check r = client.post(f"{AUTH_URI}/login", json=data) assert r.status_code == 401 resp = self.get_content(r) assert resp == "Invalid access credentials" data = { "username": BaseTests.unused_credentials[0], "password": BaseTests.unused_credentials[1], } # Login is blocked due to inactivity r = client.post(f"{AUTH_URI}/login", json=data) assert r.status_code == 403 resp = self.get_content(r) assert resp == "Sorry, this account is blocked for inactivity" # Also password reset and blocked... how to recover the account !? reset_data = {"reset_email": BaseTests.unused_credentials[0]} r = client.post(f"{AUTH_URI}/reset", json=reset_data) assert r.status_code == 403 resp = self.get_content(r) assert resp == "Sorry, this account is blocked for inactivity" events = self.get_last_events(2) assert events[0].event == Events.refused_login.value assert events[0].payload[ "username"] == BaseTests.unused_credentials[0] assert (events[0].payload["motivation"] == "account blocked due to inactivity") assert events[1].event == Events.refused_login.value assert events[1].payload[ "username"] == BaseTests.unused_credentials[0] assert (events[1].payload["motivation"] == "account blocked due to inactivity") assert events[1].url == "/auth/reset" # Goodbye temporary user self.delete_user(client, BaseTests.unused_credentials[2])
def test_database_exceptions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping dabase exceptions tests") return # This is a special value. The endpoint will try to create a group without # shortname. A BadRequest is expected because the database should refuse the # entry due to the missing property r = client.post(f"{API_URI}/tests/database/400") assert r.status_code == 400 # This is the message of a DatabaseMissingRequiredProperty assert self.get_content(r) == "Missing property shortname required by Group" auth = Connector.get_authentication_instance() default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # the /tests/database endpoint will change the default group fullname # as a side effect to the test the database_transaction decorator default_fullname = default_group.fullname random_name = faker.pystr() # This will create a new group with short/full name == random_name r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 200 default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # As a side effect the fullname of defaut_group is changed... assert default_group.fullname != default_fullname # ... and this is the new name new_fullname = default_group.fullname # This will try to create again a group with short/full name == random_name # but this will fail due to unique keys r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 409 # This is the message of a DatabaseDuplicatedEntry self.get_content(r) == "A Group already exists with 'shortname': '400'" # The default group will not change again because the # database_transaction decorator will undo the change default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None assert default_group.fullname == new_fullname
def test_autocomplete(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/autocomplete", {}) assert schema[0]["key"] == "elements" assert schema[0]["type"] == "string[]" assert "autocomplete_endpoint" in schema[0] assert "autocomplete_id_bind" in schema[0] assert "autocomplete_label_bind" in schema[0] assert "autocomplete_show_id" in schema[0] assert schema[0]["autocomplete_endpoint"] == "/api/tests/autocomplete" assert schema[0]["autocomplete_id_bind"] == "my_id" assert schema[0]["autocomplete_label_bind"] == "my_label" assert schema[0]["autocomplete_show_id"] is True autocomplete_endpoint = f"{SERVER_URI}{schema[0]['autocomplete_endpoint']}" r = client.get(f"{autocomplete_endpoint}/nobody") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 0 r = client.get(f"{autocomplete_endpoint}/oliver") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) > 0 assert schema[0]["autocomplete_id_bind"] in content[0] assert schema[0]["autocomplete_label_bind"] in content[0] r = client.get(f"{autocomplete_endpoint}/s the") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) > 0 assert schema[0]["autocomplete_id_bind"] in content[0] assert schema[0]["autocomplete_label_bind"] in content[0] rand = random.SystemRandom() data = [] for _ in range(0, 3): element = rand.choice(content) data.append(element[schema[0]["autocomplete_id_bind"]]) # put accepts a single id provided by the autocomplete endpoint r = client.put(f"{API_URI}/tests/autocomplete", json={"element": data[0]}) assert r.status_code == 204 # post accepts a list of ids provided by the autocomplete endpoint r = client.post( f"{API_URI}/tests/autocomplete", json={"elements": orjson.dumps(data).decode("UTF8")}, ) assert r.status_code == 204
def test_01_login_ban_not_enabled(self, client: FlaskClient) -> None: uuid, data = self.create_user(client) # Login attempts are not registered, let's try to fail the login many times for _ in range(0, 10): self.do_login(client, data["email"], "wrong", status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == data["email"] assert events[0].url == "/auth/login" # and verify that login is still allowed headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == data["email"] assert events[0].url == "/auth/login" # Furthermore the login/unlock endpoint is now enabled r = client.post(f"{AUTH_URI}/login/unlock/token") assert r.status_code == 404 # Goodbye temporary user self.delete_user(client, uuid)
def upload_file( client: FlaskClient, headers: Dict[str, str], fastq: Path, dataset_uuid: str, stream: bool = True, ) -> Response: # get the data for the upload request filename = fastq.name filesize = fastq.stat().st_size data = { "name": filename, "mimeType": "application/gzip", "size": filesize, "lastModified": int(fastq.stat().st_mtime), } r_post: Response = client.post( f"{API_URI}/dataset/{dataset_uuid}/files/upload", headers=headers, data=data) if r_post.status_code != 201: return r_post chunksize = int(filesize / 2) + 1 range_start = 0 with open(fastq, "rb") as f: while True: read_data = f.read(chunksize) # No more data to send, exit the loop # This case is never reached because in normal conditions # the loop is exited when the APIs respond with a code != 206 if not read_data: # pragma: no cover break if range_start != 0: range_start += 1 range_max = range_start + chunksize if range_max > filesize: range_max = filesize headers[ "Content-Range"] = f"bytes {range_start}-{range_max}/{filesize}" if stream: r: Response = client.put( f"{API_URI}/dataset/{dataset_uuid}/files/upload/{filename}", headers=headers, data=read_data, ) else: # do not read data to test final size!=expected size r = client.put( f"{API_URI}/dataset/{dataset_uuid}/files/upload/{filename}", headers=headers, ) if r.status_code != 206: # the upload is completed or an error occurred break range_start += chunksize return r
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.getDynamicInputSchema(client, "tests/inputs", {}) data = self.buildData(schema) r = client.post(f"{API_URI}/tests/inputs", data=data) assert r.status_code == 204
def check_endpoint( self, client: FlaskClient, method: str, endpoint: str, headers: Optional[Dict[str, str]], expected_authorized: bool, paths: List[str], ) -> List[str]: assert method in ( "GET", "POST", "PUT", "PATCH", "DELETE", ) path = self.get_path(method, endpoint) assert path in paths # SERVER_URI because api and auth are already included in endpoint full_endpoint = f"{SERVER_URI}/{endpoint}" if method == "GET": r = client.get(full_endpoint, headers=headers) elif method == "POST": r = client.post(full_endpoint, headers=headers) elif method == "PUT": r = client.put(full_endpoint, headers=headers) elif method == "PATCH": r = client.patch(full_endpoint, headers=headers) elif method == "DELETE": r = client.delete(full_endpoint, headers=headers) else: # pragma: no cover pytest.fail("Unknown method") if expected_authorized: assert r.status_code != 401 else: assert r.status_code != 400 paths.remove(path) return paths
def test_outputs(self, client: FlaskClient) -> None: r = client.post(f"{API_URI}/tests/outputs/string") assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, str) assert response == "string" r = client.post(f"{API_URI}/tests/outputs/whatever") assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, str) assert response == "string" r = client.post(f"{API_URI}/tests/outputs/list") assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert response == ["a", "b", "c", "c"] r = client.post(f"{API_URI}/tests/outputs/tuple") assert r.status_code == 200 response = self.get_content(r) # Tuples are serialized as lists assert isinstance(response, list) assert response == ["a", "b", "c", "c"] r = client.post(f"{API_URI}/tests/outputs/set") assert r.status_code == 200 response = self.get_content(r) # Sets are serialized as lists assert isinstance(response, list) # But without duplicates :-) (and unordered...) assert sorted(response) == ["a", "b", "c"] r = client.post(f"{API_URI}/tests/outputs/dict") assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert response == {"a": 1, "b": 2, "c": 3} r = client.post(f"{API_URI}/tests/outputs/datetime") assert r.status_code == 200 response = self.get_content(r) # datetimes are serialized as strings assert isinstance(response, str)
def test_admin_users(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE") or not Env.get_bool( "AUTH_ENABLE"): log.warning("Skipping admin/users tests") return project_tile = get_project_configuration("project.title", default="YourProject") auth = Connector.get_authentication_instance() staff_role_enabled = Role.STAFF.value in [ r.name for r in auth.get_roles() ] for role in ( Role.ADMIN, Role.STAFF, ): if not staff_role_enabled: # pragma: no cover log.warning( "Skipping tests of admin/users endpoints, role Staff not enabled" ) continue else: log.warning("Testing admin/users endpoints as {}", role) if role == Role.ADMIN: user_email = BaseAuthentication.default_user user_password = BaseAuthentication.default_password elif role == Role.STAFF: _, user_data = self.create_user(client, roles=[Role.STAFF]) user_email = user_data.get("email") user_password = user_data.get("password") headers, _ = self.do_login(client, user_email, user_password) r = client.get(f"{API_URI}/admin/users", headers=headers) assert r.status_code == 200 schema = self.get_dynamic_input_schema(client, "admin/users", headers) data = self.buildData(schema) data["email_notification"] = True data["is_active"] = True data["expiration"] = None # Event 1: create r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 200 uuid = self.get_content(r) assert isinstance(uuid, str) # A new User is created events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.create.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].url == "/api/admin/users" assert "name" in events[0].payload assert "surname" in events[0].payload assert "email" in events[0].payload # Save it for the following tests event_target_id1 = events[0].target_id mail = self.read_mock_email() body = mail.get("body", "") # Subject: is a key in the MIMEText assert body is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: New credentials" in mail.get( "headers", "") assert data.get("email", "MISSING").lower() in body assert (data.get("password", "MISSING") in body or escape(str(data.get("password"))) in body) # Test the differences between post and put schema post_schema = {s["key"]: s for s in schema} tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{uuid}", headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "email" in post_schema assert post_schema["email"]["required"] assert "email" not in put_schema assert "name" in post_schema assert post_schema["name"]["required"] assert "name" in put_schema assert not put_schema["name"]["required"] assert "surname" in post_schema assert post_schema["surname"]["required"] assert "surname" in put_schema assert not put_schema["surname"]["required"] assert "password" in post_schema assert post_schema["password"]["required"] assert "password" in put_schema assert not put_schema["password"]["required"] assert "group" in post_schema assert post_schema["group"]["required"] assert "group" in put_schema assert not put_schema["group"]["required"] # Event 2: read r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, dict) assert len(users_list) > 0 # email is saved lowercase assert users_list.get("email") == data.get("email", "MISSING").lower() # Access to the user events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.access.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert len(events[0].payload) == 0 # Check duplicates r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 409 assert (self.get_content(r) == f"A User already exists with email: {data['email']}") data["email"] = BaseAuthentication.default_user r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 409 assert ( self.get_content(r) == f"A User already exists with email: {BaseAuthentication.default_user}" ) # Create another user data2 = self.buildData(schema) data2["email_notification"] = True data2["is_active"] = True data2["expiration"] = None # Event 3: create r = client.post(f"{API_URI}/admin/users", json=data2, headers=headers) assert r.status_code == 200 uuid2 = self.get_content(r) assert isinstance(uuid2, str) # Another User is created events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.create.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id != event_target_id1 assert events[0].url == "/api/admin/users" assert "name" in events[0].payload assert "surname" in events[0].payload assert "email" in events[0].payload # Save it for the following tests event_target_id2 = events[0].target_id mail = self.read_mock_email() body = mail.get("body", "") # Subject: is a key in the MIMEText assert body is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: New credentials" in mail.get( "headers", "") assert data2.get("email", "MISSING").lower() in body pwd = data2.get("password", "MISSING") assert pwd in body or escape(str(pwd)) in body # send and invalid user_id r = client.put( f"{API_URI}/admin/users/invalid", json={"name": faker.name()}, headers=headers, ) assert r.status_code == 404 # Event 4: modify r = client.put( f"{API_URI}/admin/users/{uuid}", json={"name": faker.name()}, headers=headers, ) assert r.status_code == 204 # User 1 modified (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert "name" in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" not in events[0].payload # email cannot be modified new_data = {"email": data.get("email")} r = client.put(f"{API_URI}/admin/users/{uuid2}", json=new_data, headers=headers) # from webargs >= 6 this endpoint no longer return a 204 but a 400 # because email is an unknown field # assert r.status_code == 204 assert r.status_code == 400 # Event 5: read r = client.get(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, dict) assert len(users_list) > 0 # email is not modified -> still equal to data2, not data1 assert users_list.get("email") != data.get("email", "MISSING").lower() assert users_list.get("email") == data2.get("email", "MISSING").lower() # Access to user 2 events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.access.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert len(events[0].payload) == 0 r = client.delete(f"{API_URI}/admin/users/invalid", headers=headers) assert r.status_code == 404 # Event 6: delete r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 204 # User 1 is deleted (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.delete.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert len(events[0].payload) == 0 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 404 # change password of user2 # Event 7: modify newpwd = faker.password(strong=True) data = {"password": newpwd, "email_notification": True} r = client.put(f"{API_URI}/admin/users/{uuid2}", json=data, headers=headers) assert r.status_code == 204 # User 2 modified (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert "name" not in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" in events[0].payload assert "email_notification" in events[0].payload # Verify that the password is obfuscated in the log: assert events[0].payload["password"] == OBSCURE_VALUE mail = self.read_mock_email() # Subject: is a key in the MIMEText assert mail.get("body", "") is not None assert mail.get("headers", "") is not None assert f"Subject: {project_tile}: Password changed" in mail.get( "headers", "") assert data2.get("email", "MISSING").lower() in mail.get("body", "") assert newpwd in mail.get( "body", "") or escape(newpwd) in mail.get("body", "") # login with a newly created user headers2, _ = self.do_login(client, data2.get("email"), newpwd) # normal users cannot access to this endpoint r = client.get(f"{API_URI}/admin/users", headers=headers2) assert r.status_code == 401 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 r = client.post(f"{API_URI}/admin/users", json=data, headers=headers2) assert r.status_code == 401 r = client.put( f"{API_URI}/admin/users/{uuid}", json={"name": faker.name()}, headers=headers2, ) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 # Users are not authorized to /admin/tokens # These two tests should be moved in test_endpoints_tokens.py r = client.get(f"{API_URI}/admin/tokens", headers=headers2) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/tokens/xyz", headers=headers2) assert r.status_code == 401 # let's delete the second user # Event 8: delete r = client.delete(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 204 # User 2 is deleted (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.delete.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert len(events[0].payload) == 0 # Restore the default password (changed due to FORCE_FIRST_PASSWORD_CHANGE) # or MAX_PASSWORD_VALIDITY errors r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) uuid = str(content.get("uuid")) data = { "password": user_password, # very important, otherwise the default user will lose its role "roles": orjson.dumps([role]).decode("UTF8"), } # Event 9: modify r = client.put(f"{API_URI}/admin/users/{uuid}", json=data, headers=headers) assert r.status_code == 204 # Default user is modified events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id != event_target_id1 assert events[0].target_id != event_target_id2 assert events[0].url != f"/api/admin/users/{event_target_id1}" assert events[0].url != f"/api/admin/users/{event_target_id2}" assert "name" not in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" in events[0].payload assert "roles" in events[0].payload assert "email_notification" not in events[0].payload # Verify that the password is obfuscated in the log: assert events[0].payload["password"] == OBSCURE_VALUE r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204
def test_staff_restrictions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE") or not Env.get_bool( "AUTH_ENABLE"): log.warning("Skipping admin/users tests") return auth = Connector.get_authentication_instance() staff_role_enabled = Role.STAFF.value in [ r.name for r in auth.get_roles() ] if not staff_role_enabled: # pragma: no cover log.warning( "Skipping tests of admin/users restrictions, role Staff not enabled" ) return staff_uuid, staff_data = self.create_user(client, roles=[Role.STAFF]) staff_email = staff_data.get("email") staff_password = staff_data.get("password") staff_headers, _ = self.do_login(client, staff_email, staff_password) user_uuid, _ = self.create_user(client, roles=[Role.USER]) admin_headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) admin_uuid = content.get("uuid") # Staff users are not allowed to retrieve Admins' data r = client.get(f"{API_URI}/admin/users/{user_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{staff_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{admin_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{user_uuid}", headers=staff_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{staff_uuid}", headers=staff_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{admin_uuid}", headers=staff_headers) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" # Staff users are not allowed to edit Admins r = client.put( f"{API_URI}/admin/users/{admin_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.STAFF]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" r = client.put( f"{API_URI}/admin/users/{staff_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.STAFF]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 204 r = client.put( f"{API_URI}/admin/users/{user_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.USER]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 204 # Admin role is not allowed for Staff users tmp_schema = self.get_dynamic_input_schema(client, "admin/users", admin_headers) post_schema = {s["key"]: s for s in tmp_schema} assert "roles" in post_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{user_uuid}", admin_headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "roles" in put_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, "admin/users", staff_headers) post_schema = {s["key"]: s for s in tmp_schema} assert "roles" in post_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" not in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{user_uuid}", staff_headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "roles" in put_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" not in post_schema["roles"]["options"] # Staff can't send role admin on put r = client.put( f"{API_URI}/admin/users/{user_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.ADMIN]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 400 # Staff can't send role admin on post schema = self.get_dynamic_input_schema(client, "admin/users", staff_headers) data = self.buildData(schema) data["email_notification"] = True data["is_active"] = True data["expiration"] = None data["roles"] = orjson.dumps([Role.ADMIN]).decode("UTF8") r = client.post(f"{API_URI}/admin/users", json=data, headers=staff_headers) assert r.status_code == 400 # Admin users are filtered out when asked from a Staff user r = client.get(f"{API_URI}/admin/users", headers=admin_headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, list) assert len(users_list) > 0 email_list = [u.get("email") for u in users_list] assert staff_email in email_list assert BaseAuthentication.default_user in email_list r = client.get(f"{API_URI}/admin/users", headers=staff_headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, list) assert len(users_list) > 0 email_list = [u.get("email") for u in users_list] assert staff_email in email_list assert BaseAuthentication.default_user not in email_list # Staff users are not allowed to delete Admins r = client.delete(f"{API_URI}/admin/users/{admin_uuid}", headers=staff_headers) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" r = client.delete(f"{API_URI}/admin/users/{user_uuid}", headers=staff_headers) assert r.status_code == 204
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
def test_chunked_upload_and_download(self, client: FlaskClient, faker: Faker) -> None: warnings.filterwarnings( "ignore", message="unclosed file <_io.BufferedReader name=") self.fname = self.get("fname") self.fcontent = self.get("fcontent") # as defined in test_upload.py for chunked uploads upload_folder = "fixed" r = client.post(f"{API_URI}/tests/chunkedupload", data={"force": True}) assert r.status_code == 400 filename = "fixed.filename.txt" data = { "force": True, "name": filename, "size": "999", "mimeType": "application/zip", "lastModified": 1590302749209, } r = client.post(f"{API_URI}/tests/chunkedupload", data=data) assert r.status_code == 201 assert self.get_content(r) == "" upload_endpoint = get_location_header( r.headers, expected=f"{API_URI}/tests/chunkedupload/{filename}") data["force"] = False r = client.post(f"{API_URI}/tests/chunkedupload", data=data) assert r.status_code == 409 assert self.get_content(r) == f"File '{filename}' already exists" with io.StringIO(faker.text()) as f: r = client.put(upload_endpoint, data=f) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" with io.StringIO(faker.text()) as f: r = client.put( upload_endpoint, data=f, headers={"Content-Range": "!"}, ) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" up_data = faker.pystr(min_chars=24, max_chars=48).lower() STR_LEN = len(up_data) with io.StringIO(up_data[0:5]) as f: r = client.put( upload_endpoint, data=f, headers={"Content-Range": f"bytes 0-5/{STR_LEN}"}, ) assert r.status_code == 206 assert self.get_content(r) == "partial" destination_path = DATA_PATH.joinpath(upload_folder, filename) assert destination_path.exists() # The file is still writeable because the upload is in progress assert oct(os.stat(destination_path).st_mode & 0o777) != "0o440" with io.StringIO(up_data[5:]) as f: r = client.put( upload_endpoint, data=f, headers={"Content-Range": f"bytes 5-{STR_LEN}/{STR_LEN}"}, ) assert r.status_code == 200 c = self.get_content(r) assert isinstance(c, dict) assert c.get("filename") is not None uploaded_filename = c.get("filename") meta = c.get("meta") assert meta is not None assert meta.get("charset") == "us-ascii" assert meta.get("type") == "text/plain" destination_path = DATA_PATH.joinpath(upload_folder, filename) assert destination_path.exists() assert oct(os.stat(destination_path).st_mode & 0o777) == "0o440" r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": ""}, ) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": f"0-{STR_LEN - 1}"}, ) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": "bytes=0-9999999999999999"}, ) assert r.status_code == 206 r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": "bytes=0-4"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[0:5] r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": f"bytes=5-{STR_LEN - 1}"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[5:] r = client.get( f"{API_URI}/tests/download/{upload_folder}/{uploaded_filename}", headers={"Range": f"bytes=0-{STR_LEN - 1}"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data # Send a new string as content file. Will be appended as prefix up_data2 = faker.pystr(min_chars=24, max_chars=48) STR_LEN = len(up_data2) with io.StringIO(up_data2) as f: r = client.put( upload_endpoint, data=f, headers={"Content-Range": f"bytes */{STR_LEN}"}, ) assert r.status_code == 503 assert self.get_content( r) == "Permission denied: failed to write the file" # force the file to be writeable again destination_path = DATA_PATH.joinpath(upload_folder, filename) # -rw-rw---- destination_path.chmod(0o660) with io.StringIO(up_data2) as f: r = client.put( upload_endpoint, data=f, headers={"Content-Range": f"bytes */{STR_LEN}"}, ) assert r.status_code == 200 destination_path = DATA_PATH.joinpath(upload_folder, filename) assert destination_path.exists() # File permissions are restored assert oct(os.stat(destination_path).st_mode & 0o777) == "0o440" # c = self.get_content(r) # assert c.get('filename') is not None # uploaded_filename = c.get('filename') # meta = c.get('meta') # assert meta is not None # assert meta.get('charset') == 'us-ascii' # assert meta.get('type') == 'text/plain' # r = client.get( # f'{API_URI}/tests/download/{upload_folder}/{uploaded_filename}' # ) # assert r.status_code == 200 # content = r.data.decode('utf-8') # # Uhmmm... should not be up_data2 + up_data ?? # assert content == up_data + up_data2 data["force"] = False r = client.post(f"{API_URI}/tests/chunkedupload", data=data) assert r.status_code == 409 err = f"File '{uploaded_filename}' already exists" assert self.get_content(r) == err data["force"] = True r = client.post(f"{API_URI}/tests/chunkedupload", data=data) assert r.status_code == 201 assert self.get_content(r) == "" upload_endpoint = get_location_header( r.headers, expected=f"{API_URI}/tests/chunkedupload/{filename}") data["name"] = "fixed.filename.notallowed" data["force"] = False r = client.post(f"{API_URI}/tests/chunkedupload", data=data) assert r.status_code == 400 assert self.get_content(r) == "File extension not allowed" # Send an upload on a file endpoint not previously initialized filename = f"{faker.pystr()}.txt" with io.StringIO(up_data2) as f: r = client.put( f"{API_URI}/tests/chunkedupload/{filename}", data=f, headers={"Content-Range": f"bytes */{STR_LEN}"}, ) assert r.status_code == 503 error = "Permission denied: the destination file does not exist" assert self.get_content(r) == error destination_path = DATA_PATH.joinpath(upload_folder, filename) assert not destination_path.exists()
def test_password_reset(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("ALLOW_PASSWORD_RESET") or not Env.get_bool("AUTH_ENABLE"): log.warning("Password reset is disabled, skipping tests") return project_tile = get_project_configuration("project.title", default="YourProject") proto = "https" if PRODUCTION else "http" # Request password reset, missing information r = client.post(f"{AUTH_URI}/reset") assert r.status_code == 400 # Request password reset, missing information r = client.post(f"{AUTH_URI}/reset", json=faker.pydict(2)) assert r.status_code == 400 headers, _ = self.do_login(client, None, None) # Request password reset, wrong email wrong_email = faker.ascii_email() data = {"reset_email": wrong_email} r = client.post(f"{AUTH_URI}/reset", json=data) assert r.status_code == 403 msg = f"Sorry, {wrong_email} is not recognized as a valid username" assert self.get_content(r) == msg # Request password reset, correct email data = {"reset_email": BaseAuthentication.default_user} r = client.post(f"{AUTH_URI}/reset", json=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.reset_password_request.value assert events[0].user == data["reset_email"] assert events[0].url == "/auth/reset" resetmsg = "We'll send instructions to the email provided " resetmsg += "if it's associated with an account. " resetmsg += "Please check your spam/junk folder." assert self.get_content(r) == resetmsg mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile}: Password Reset" in mail.get("headers", "") assert f"{proto}://localhost/public/reset/" in body token = self.get_token_from_body(body) assert token is not None # Do password reset r = client.put(f"{AUTH_URI}/reset/thisisatoken", json={}) # this token is not valid assert r.status_code == 400 # Check if token is valid r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 204 # Token is still valid because no password still sent r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 204 # Missing information data = { "new_password": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "Invalid password" data = { "password_confirm": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "Invalid password" # Request with old password data = { "new_password": BaseAuthentication.default_password, "password_confirm": BaseAuthentication.default_password, } r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 409 error = "The new password cannot match the previous password" assert self.get_content(r) == error min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) # Password too short data["new_password"] = faker.password(min_pwd_len - 1) data["password_confirm"] = faker.password(min_pwd_len - 1) r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 data["new_password"] = faker.password(min_pwd_len, strong=True) data["password_confirm"] = faker.password(min_pwd_len, strong=True) r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 400 assert self.get_content(r) == "New password does not match with confirmation" new_pwd = faker.password(min_pwd_len, strong=True) data["new_password"] = new_pwd data["password_confirm"] = new_pwd r = client.put(f"{AUTH_URI}/reset/{token}", json=data) assert r.status_code == 200 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break self.do_login(client, None, None, status_code=401) headers, _ = self.do_login(client, None, new_pwd) # Token is no longer valid r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Restore the default password if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp(BaseAuthentication.default_user) data["password"] = new_pwd data["new_password"] = BaseAuthentication.default_password data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", json=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change password event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == BaseAuthentication.default_user break self.do_login(client, None, new_pwd, status_code=401) self.do_login(client, None, None) # Token created for another user token = self.get_crafted_token("r") r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Token created for another user token = self.get_crafted_token("r", wrong_algorithm=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Token created for another user token = self.get_crafted_token("r", wrong_secret=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) uuid = response.get("uuid") token = self.get_crafted_token("x", user_id=uuid) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("r", user_id=uuid) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Immature token token = self.get_crafted_token("r", user_id=uuid, immature=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token" # Expired token token = self.get_crafted_token("r", user_id=uuid, expired=True) r = client.put(f"{AUTH_URI}/reset/{token}", json={}) assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid reset token: this request is expired"
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"
def test_api_phenotype(self, client: FlaskClient, faker: Faker) -> None: # setup the test env ( admin_headers, uuid_group_A, user_A1_uuid, user_A1_headers, uuid_group_B, user_B1_uuid, user_B1_headers, user_B2_uuid, user_B2_headers, study1_uuid, study2_uuid, ) = create_test_env(client, faker, study=True) # create a new phenotype with wrong age phenotype1 = { "name": faker.pystr(), "age": -2, "sex": "male", } r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype1, ) assert r.status_code == 400 # create a new phenotype phenotype1["age"] = faker.pyint(0, 100) r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype1, ) assert r.status_code == 200 phenotype1_uuid = self.get_content(r) assert isinstance(phenotype1_uuid, str) # create a new phenotype in a study of an other group r = client.post( f"{API_URI}/study/{study2_uuid}/phenotypes", headers=user_B1_headers, data=phenotype1, ) assert r.status_code == 404 # create a new phenotype as admin not belonging to study group phenotype2 = { "name": faker.pystr(), "age": faker.pyint(0, 100), "sex": "female", } r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=admin_headers, data=phenotype2, ) assert r.status_code == 404 # create a phenotype with a geodata and a list of hpo graph = neo4j.get_instance() geodata_nodes = graph.GeoData.nodes geodata_uuid = geodata_nodes[0].uuid hpo_nodes = graph.HPO.nodes hpo1_id = hpo_nodes[0].hpo_id hpo2_id = hpo_nodes[1].hpo_id phenotype2["birth_place"] = geodata_uuid phenotype2["hpo"] = [hpo1_id, hpo2_id] phenotype2["hpo"] = json.dumps(phenotype2["hpo"]) r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype2, ) assert r.status_code == 200 phenotype2_uuid = self.get_content(r) assert isinstance(phenotype2_uuid, str) # test phenotype access # test phenotype list response r = client.get(f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 2 # test phenotype list response for a study you don't have access r = client.get(f"{API_URI}/study/{study2_uuid}/phenotypes", headers=user_B1_headers) assert r.status_code == 404 # test phenotype list response for admin r = client.get(f"{API_URI}/study/{study1_uuid}/phenotypes", headers=admin_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 2 # test empty list of phenotypes in a study r = client.get(f"{API_URI}/study/{study2_uuid}/phenotypes", headers=user_A1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert not response # study owner r = client.get(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_B1_headers) assert r.status_code == 200 # same group of the study owner r = client.get(f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B2_headers) assert r.status_code == 200 # check hpo and geodata were correctly linked response = self.get_content(r) assert isinstance(response, dict) assert response["birth_place"]["uuid"] == geodata_uuid assert len(response["hpo"]) == 2 hpo_list = [] for el in response["hpo"]: hpo_list.append(el["hpo_id"]) assert hpo1_id in hpo_list assert hpo2_id in hpo_list # phenotype owned by an other group r = client.get(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_A1_headers) assert r.status_code == 404 not_authorized_message = self.get_content(r) assert isinstance(not_authorized_message, str) # admin access r = client.get(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=admin_headers) assert r.status_code == 200 # test phenotype modification # modify a non existent phenotype random_phenotype = faker.pystr() r = client.put( f"{API_URI}/phenotype/{random_phenotype}", headers=user_A1_headers, data={ "name": faker.pystr(), "sex": "female" }, ) assert r.status_code == 404 # modify a phenotype you do not own r = client.put( f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_A1_headers, data={ "name": faker.pystr(), "sex": "female" }, ) assert r.status_code == 404 # modify a phenotype using a wrong age phenotype1["age"] = -8 r = client.put( f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_B1_headers, data=phenotype1, ) assert r.status_code == 400 # modify a phenotype you own phenotype1["age"] = faker.pyint(0, 100) r = client.put( f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_B1_headers, data=phenotype1, ) assert r.status_code == 204 # admin modify a phenotype of a group he don't belongs r = client.put( f"{API_URI}/phenotype/{phenotype1_uuid}", headers=admin_headers, data={ "name": faker.pystr(), "sex": "female" }, ) assert r.status_code == 404 # add a new hpo and change the previous geodata hpo3_id = hpo_nodes[2].hpo_id geodata2_uuid = geodata_nodes[1].uuid phenotype2["name"] = faker.pystr() phenotype2["sex"] = "male" phenotype2["birth_place"] = geodata2_uuid phenotype2["hpo"] = [hpo1_id, hpo2_id, hpo3_id] phenotype2["hpo"] = json.dumps(phenotype2["hpo"]) r = client.put( f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B1_headers, data=phenotype2, ) assert r.status_code == 204 r = client.get(f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B2_headers) res = self.get_content(r) assert isinstance(res, dict) assert res["birth_place"]["uuid"] == geodata2_uuid assert len(res["hpo"]) == 3 # delete all hpo and geodata data: Dict[str, Any] = {**phenotype2} data.pop("birth_place", None) data.pop("hpo", None) r = client.put(f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B1_headers, data=data) assert r.status_code == 204 r = client.get(f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B2_headers) response = self.get_content(r) assert isinstance(response, dict) assert "birth_place" not in response assert not response["hpo"] # add a no existing geodata phenotype2["birth_place"] = faker.pystr() r = client.put( f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B1_headers, data=phenotype2, ) assert r.status_code == 400 # delete a phenotype # delete a phenotype that does not exists r = client.delete(f"{API_URI}/phenotype/{random_phenotype}", headers=user_A1_headers) assert r.status_code == 404 # delete a phenotype in a study you do not own r = client.delete(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_A1_headers) assert r.status_code == 404 # admin delete a phenotype of a group he don't belong r = client.delete(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=admin_headers) assert r.status_code == 404 # delete a phenotype in a study you own r = client.delete(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_B1_headers) assert r.status_code == 204 # delete a phenotype in a study own by your group r = client.delete(f"{API_URI}/phenotype/{phenotype2_uuid}", headers=user_B2_headers) assert r.status_code == 204 # check phenotype deletion r = client.get(f"{API_URI}/phenotype/{phenotype1_uuid}", headers=user_B1_headers) assert r.status_code == 404 not_existent_message = self.get_content(r) assert isinstance(not_existent_message, str) assert not_existent_message == not_authorized_message # delete all the elements used by the test delete_test_env( client, user_A1_headers, user_B1_headers, user_B1_uuid, user_B2_uuid, user_A1_uuid, uuid_group_A, uuid_group_B, study1_uuid=study1_uuid, study2_uuid=study2_uuid, )
def test_03_registration_and_login_ban( self, client: FlaskClient, faker: Faker ) -> None: if Env.get_bool("ALLOW_REGISTRATION"): registration_data = {} registration_data["email"] = faker.ascii_email() registration_data["name"] = faker.first_name() registration_data["surname"] = faker.last_name() registration_data["password"] = faker.password(strong=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", json=registration_data) # now the user is created but INACTIVE, activation endpoint is needed assert r.status_code == 200 registration_message = "We are sending an email to your email address " registration_message += "where you will find the link to activate " registration_message += "your account" assert self.get_content(r) == registration_message # Registration endpoint send 2 mail: the first is the activation link, # the second (last) is the admin notification mail = self.read_mock_email(previous=True) body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText proto = "https" if PRODUCTION else "http" assert f"{proto}://localhost/public/register/" in body token = self.get_token_from_body(body) assert token is not None # 403 because the account is not activated self.do_login( client, registration_data["email"], registration_data["password"], status_code=403, ) events = self.get_last_events(1) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == registration_data["email"] assert events[0].payload["motivation"] == "account not active" assert events[0].url == "/auth/login" self.delete_mock_email() for _ in range(0, max_login_attempts): # Event if non activated if password is wrong the status is 401 self.do_login( client, registration_data["email"], "wrong", status_code=401, ) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == registration_data["email"] assert events[0].url == "/auth/login" self.verify_credentials_ban_notification() # After max_login_attempts the account is not blocked # profile activation forbidden due to blocked acount r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 403 assert self.get_content(r) == BAN_MESSAGE events = self.get_last_events(1) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == registration_data["email"] assert ( events[0].payload["motivation"] == "account blocked due to too many failed logins" ) assert events[0].url == f"/auth/profile/activate/{token}" # request activation forbidden due to blocked acount r = client.post( f"{AUTH_URI}/profile/activate", json={"username": registration_data["email"]}, ) assert r.status_code == 403 assert self.get_content(r) == BAN_MESSAGE events = self.get_last_events(1) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == registration_data["email"] assert ( events[0].payload["motivation"] == "account blocked due to too many failed logins" ) assert events[0].url == "/auth/profile/activate" time.sleep(ban_duration) r = client.post( f"{AUTH_URI}/profile/activate", json={"username": registration_data["email"]}, ) assert r.status_code == 200
def test_02_GET_profile(self, client: FlaskClient, faker: Faker) -> None: """ Check if you can use your token for protected endpoints """ # Check success log.info("*** VERIFY valid token") r = client.get(f"{AUTH_URI}/profile", headers=self.get("auth_header")) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") # Check failure log.info("*** VERIFY invalid token") r = client.get(f"{AUTH_URI}/profile") assert r.status_code == 401 # Token created for a fake user token = self.get_crafted_token("f") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("x") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_algorithm=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Token created for another user token = self.get_crafted_token("f", wrong_secret=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # token created for the correct user, but from outside the system!! token = self.get_crafted_token("f", user_id=uuid) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Immature token token = self.get_crafted_token("f", user_id=uuid, immature=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Expired token token = self.get_crafted_token("f", user_id=uuid, expired=True) headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 401 # Sending malformed tokens headers = {"Authorization": "Bearer"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"Bearer '{faker.pystr()}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 # Bearer realm is expected to be case sensitive token = self.get("auth_token") headers = {"Authorization": f"Bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 headers = {"Authorization": f"bearer {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 headers = {"Authorization": f"BEARER {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 token = self.get("auth_token") headers = {"Authorization": f"Bear {token}"} r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401 USER = BaseAuthentication.default_user PWD = BaseAuthentication.default_password # Testing Basic Authentication (not allowed) credentials = f"{USER}:{PWD}" encoded_credentials = base64.b64encode( str.encode(credentials)).decode("utf-8") headers = {"Authorization": f"Basic {encoded_credentials}"} r = client.post(f"{AUTH_URI}/login", headers=headers) # Response is: # { # 'password': ['Missing data for required field.'], # 'username': ['Missing data for required field.'] # } assert r.status_code == 400 r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 401
def test_api_family(self, client: FlaskClient, faker: Faker) -> None: # setup the test env ( admin_headers, uuid_group_A, user_A1_uuid, user_A1_headers, uuid_group_B, user_B1_uuid, user_B1_headers, user_B2_uuid, user_B2_headers, study1_uuid, study2_uuid, ) = create_test_env(client, faker, study=True) # create new phenotypes phenotype_father = { "name": faker.pystr(), "age": faker.pyint(0, 100), "sex": "male", } r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype_father, ) assert r.status_code == 200 phenotype_father_uuid = self.get_content(r) assert isinstance(phenotype_father_uuid, str) phenotype_mother = { "name": faker.pystr(), "age": faker.pyint(0, 100), "sex": "female", } r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype_mother, ) assert r.status_code == 200 phenotype_mother_uuid = self.get_content(r) assert isinstance(phenotype_mother_uuid, str) phenotype_son_B = { "name": faker.pystr(), "age": faker.pyint(0, 100), "sex": "female", } r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data=phenotype_son_B, ) assert r.status_code == 200 phenotype_son_B_uuid = self.get_content(r) assert isinstance(phenotype_son_B_uuid, str) phenotype_son_A = { "name": faker.pystr(), "age": faker.pyint(0, 100), "sex": "female", } r = client.post( f"{API_URI}/study/{study2_uuid}/phenotypes", headers=user_A1_headers, data=phenotype_son_A, ) assert r.status_code == 200 phenotype_son_A_uuid = self.get_content(r) assert isinstance(phenotype_son_A_uuid, str) # create a relationship # father case r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 200 graph = neo4j.get_instance() phenotype_father_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_father_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert phenotype_father_node.son.is_connected(phenotype_son_node) assert phenotype_son_node.father.is_connected(phenotype_father_node) # test relationships in get phenotype list response r = client.get(f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) for el in response: if el["uuid"] == phenotype_son_B_uuid: assert el["relationships"]["father"][ "uuid"] == phenotype_father_uuid if el["uuid"] == phenotype_father_uuid: assert el["relationships"]["sons"][0][ "uuid"] == phenotype_son_B_uuid # test relationships in get single phenotype response r = client.get(f"{API_URI}/phenotype/{phenotype_son_B_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert response["relationships"]["father"][ "uuid"] == phenotype_father_uuid r = client.get(f"{API_URI}/phenotype/{phenotype_father_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert response["relationships"]["sons"][0][ "uuid"] == phenotype_son_B_uuid # create a relationship for two phenotypes in an other study r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=user_A1_headers, ) assert r.status_code == 404 # admin creates a relationship r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=admin_headers, ) assert r.status_code == 404 # a user of the same group of the owner create a relationship # mother case r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=user_B2_headers, ) assert r.status_code == 200 graph = neo4j.get_instance() phenotype_mother_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_mother_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert phenotype_mother_node.son.is_connected(phenotype_son_node) assert phenotype_son_node.mother.is_connected(phenotype_mother_node) # test relationships in get phenotype list response r = client.get(f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) for el in response: if el["uuid"] == phenotype_son_B_uuid: assert el["relationships"]["mother"][ "uuid"] == phenotype_mother_uuid if el["uuid"] == phenotype_mother_uuid: assert el["relationships"]["sons"][0][ "uuid"] == phenotype_son_B_uuid # test relationships in get single phenotype response r = client.get(f"{API_URI}/phenotype/{phenotype_son_B_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert response["relationships"]["mother"][ "uuid"] == phenotype_mother_uuid # relationship between phenotype from different studies r = client.post( f"{API_URI}/phenotype/{phenotype_son_A_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 # relationship with a random phenotype as son random_phenotype_uuid = faker.pystr() r = client.post( f"{API_URI}/phenotype/{random_phenotype_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 # relationship with a random phenotype as father r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{random_phenotype_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 # relationship with itself r = client.post( f"{API_URI}/phenotype/{phenotype_father_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 400 # delete a relationship # father case r = client.delete( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 204 graph = neo4j.get_instance() phenotype_father_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_father_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert not phenotype_father_node.son.single() assert not phenotype_son_node.father.single() # delete a relationship for two phenotypes in an other study r = client.delete( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=user_A1_headers, ) assert r.status_code == 404 # admin delete a relationship r = client.delete( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=admin_headers, ) assert r.status_code == 404 # a user of the same group of the owner delete a relationship # mother case r = client.delete( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=user_B2_headers, ) assert r.status_code == 204 graph = neo4j.get_instance() phenotype_mother_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_mother_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert not phenotype_mother_node.son.single() assert not phenotype_son_node.mother.single() # delete relationship between phenotype from different studies r = client.delete( f"{API_URI}/phenotype/{phenotype_son_A_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 # delete relationship with a random phenotype as son r = client.delete( f"{API_URI}/phenotype/{random_phenotype_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 # delete relationship with a random phenotype as father r = client.delete( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{random_phenotype_uuid}", headers=user_B1_headers, ) assert r.status_code == 404 r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_father_uuid}", headers=user_B1_headers, ) assert r.status_code == 200 r = client.post( f"{API_URI}/phenotype/{phenotype_son_B_uuid}/relationships/{phenotype_mother_uuid}", headers=user_B1_headers, ) assert r.status_code == 200 # delete a son relationship r = client.delete( f"{API_URI}/phenotype/{phenotype_mother_uuid}/relationships/{phenotype_son_B_uuid}", headers=user_B1_headers, ) assert r.status_code == 204 graph = neo4j.get_instance() phenotype_mother_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_mother_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert not phenotype_mother_node.son.single() assert not phenotype_son_node.mother.single() r = client.delete( f"{API_URI}/phenotype/{phenotype_father_uuid}/relationships/{phenotype_son_B_uuid}", headers=user_B1_headers, ) assert r.status_code == 204 graph = neo4j.get_instance() phenotype_father_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_father_uuid) phenotype_son_node = graph.Phenotype.nodes.get_or_none( uuid=phenotype_son_B_uuid) assert not phenotype_father_node.son.single() assert not phenotype_son_node.father.single() # delete all the elements used by the test delete_test_env( client, user_A1_headers, user_B1_headers, user_B1_uuid, user_B2_uuid, user_A1_uuid, uuid_group_A, uuid_group_B, study1_uuid=study1_uuid, study2_uuid=study2_uuid, )
def test_registration(self, client: FlaskClient, faker: Faker) -> None: # Always enabled during core tests if not Env.get_bool("ALLOW_REGISTRATION"): # pragma: no cover log.warning("User registration is disabled, skipping tests") return project_tile = get_project_configuration("project.title", default="YourProject") proto = "https" if PRODUCTION else "http" # registration, empty input r = client.post(f"{AUTH_URI}/profile") assert r.status_code == 400 # registration, missing information r = client.post(f"{AUTH_URI}/profile", data={"x": "y"}) assert r.status_code == 400 registration_data = {} registration_data["password"] = faker.password(5) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["email"] = BaseAuthentication.default_user r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["name"] = faker.first_name() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["surname"] = faker.last_name() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["password_confirm"] = faker.password(strong=True) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) registration_data["password"] = faker.password(min_pwd_len - 1) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 400 registration_data["password"] = faker.password(min_pwd_len) r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = f"This user already exists: {BaseAuthentication.default_user}" assert self.get_content(r) == m registration_data["email"] = faker.ascii_email() r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 assert self.get_content( r) == "Your password doesn't match the confirmation" registration_data["password"] = faker.password(min_pwd_len, low=False, up=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing lower case letters" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing upper case letters" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True, up=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing numbers" assert self.get_content(r) == m registration_data["password"] = faker.password(min_pwd_len, low=True, up=True, digits=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) assert r.status_code == 409 m = "Password is too weak, missing special characters" assert self.get_content(r) == m registration_data["password"] = faker.password(strong=True) registration_data["password_confirm"] = registration_data["password"] r = client.post(f"{AUTH_URI}/profile", data=registration_data) # now the user is created but INACTIVE, activation endpoint is needed assert r.status_code == 200 registration_message = "We are sending an email to your email address where " registration_message += "you will find the link to activate your account" assert self.get_content(r) == registration_message events = self.get_last_events(1) assert events[0].event == Events.create.value assert events[0].user == "-" assert events[0].target_type == "User" assert "name" in events[0].payload assert "password" in events[0].payload assert events[0].payload["password"] == OBSCURE_VALUE mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile} account activation" in mail.get( "headers") assert f"{proto}://localhost/public/register/" in body # This will fail because the user is not active _, error = self.do_login( client, registration_data["email"], registration_data["password"], status_code=403, ) assert error == "Sorry, this account is not active" # Also password reset is not allowed data = {"reset_email": registration_data["email"]} r = client.post(f"{AUTH_URI}/reset", data=data) assert r.status_code == 403 assert self.get_content(r) == "Sorry, this account is not active" events = self.get_last_events(2) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == data["reset_email"] assert events[0].payload["motivation"] == "account not active" assert events[1].event == Events.refused_login.value assert events[1].payload["username"] == data["reset_email"] assert events[1].payload["motivation"] == "account not active" # Activation, missing or wrong information r = client.post(f"{AUTH_URI}/profile/activate") assert r.status_code == 400 r = client.post(f"{AUTH_URI}/profile/activate", data=faker.pydict(2)) assert r.status_code == 400 # It isn't an email invalid = faker.pystr(10) r = client.post(f"{AUTH_URI}/profile/activate", data={"username": invalid}) assert r.status_code == 400 headers, _ = self.do_login(client, None, None) activation_message = "We are sending an email to your email address where " activation_message += "you will find the link to activate your account" # request activation, wrong username r = client.post(f"{AUTH_URI}/profile/activate", data={"username": faker.ascii_email()}) # return is 200, but no token will be generated and no mail will be sent # but it respond with the activation msg and hides the non existence of the user assert r.status_code == 200 assert self.get_content(r) == activation_message events = self.get_last_events(1) assert events[0].event != Events.activation.value assert self.read_mock_email() is None # request activation, correct username r = client.post( f"{AUTH_URI}/profile/activate", data={"username": registration_data["email"]}, ) assert r.status_code == 200 assert self.get_content(r) == activation_message mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None # Subject: is a key in the MIMEText assert f"Subject: {project_tile} account activation" in mail.get( "headers") assert f"{proto}://localhost/public/register/" in body token = self.get_token_from_body(body) assert token is not None # profile activation r = client.put(f"{AUTH_URI}/profile/activate/thisisatoken") # this token is not valid assert r.status_code == 400 # profile activation r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 200 assert self.get_content(r) == "Account activated" events = self.get_last_events(1) assert events[0].event == Events.activation.value assert events[0].user == registration_data["email"] assert events[0].target_type == "User" # Activation token is no longer valid r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 assert self.get_content(r) == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a") r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a", wrong_algorithm=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Token created for another user token = self.get_crafted_token("a", wrong_secret=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") token = self.get_crafted_token("x", user_id=uuid) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("a", user_id=uuid) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Immature token token = self.get_crafted_token("a", user_id=uuid, immature=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token" # Expired token token = self.get_crafted_token("a", user_id=uuid, expired=True) r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token: this request is expired" # Testing the following use case: # 1 - user registration # 2 - user activation using unconventional channel, e.g. by admins # 3 - user tries to activate and fails because already active registration_data["email"] = faker.ascii_email() r = client.post(f"{AUTH_URI}/profile", data=registration_data) # now the user is created but INACTIVE, activation endpoint is needed assert r.status_code == 200 mail = self.read_mock_email() body = mail.get("body") assert body is not None assert mail.get("headers") is not None assert f"{proto}://localhost/public/register/" in body token = self.get_token_from_body(body) assert token is not None headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/users", headers=headers) assert r.status_code == 200 users = self.get_content(r) uuid = None for u in users: if u.get("email") == registration_data["email"]: uuid = u.get("uuid") break assert uuid is not None r = client.put(f"{API_URI}/admin/users/{uuid}", data={"is_active": True}, headers=headers) assert r.status_code == 204 r = client.put(f"{AUTH_URI}/profile/activate/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid activation token: this request is no longer valid" r = client.get(f"{API_URI}/admin/tokens", headers=headers) content = self.get_content(r) for t in content: if t.get("token") == token: # pragma: no cover pytest.fail( "Token not properly invalidated, still bount to user {}", t.get(id))
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)"
def test_01_failed_login_ban(self, client: FlaskClient) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return uuid, data = self.create_user(client) self.delete_mock_email() for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == data["email"] assert events[0].url == "/auth/login" self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None events = self.get_last_events(1) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == data["email"] assert ( events[0].payload["motivation"] == "account blocked due to too many failed logins" ) assert events[0].url == "/auth/login" reset_data = {"reset_email": data["email"]} r = client.post(f"{AUTH_URI}/reset", json=reset_data) assert r.status_code == 403 assert self.get_content(r) == BAN_MESSAGE events = self.get_last_events(1) assert events[0].event == Events.refused_login.value assert events[0].payload["username"] == data["email"] assert ( events[0].payload["motivation"] == "account blocked due to too many failed logins" ) assert events[0].url == "/auth/reset" time.sleep(ban_duration) headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == data["email"] assert events[0].url == "/auth/login" # Verify that already emitted tokens are not blocked # 1) Block again the account for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) # 2) Verify that the account is blocked self.do_login(client, data["email"], data["password"], status_code=403) # 3) Verify that the previously emitted token is still valid r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 # Goodbye temporary user self.delete_user(client, uuid)
def test_users_custom_fields(self, client: FlaskClient) -> None: output_fields = mem.customizer.get_custom_output_fields(None) profile_inputs = mem.customizer.get_custom_input_fields( request=None, scope=mem.customizer.PROFILE) registration_inputs = mem.customizer.get_custom_input_fields( request=None, scope=mem.customizer.REGISTRATION) admin_inputs = mem.customizer.get_custom_input_fields( request=None, scope=mem.customizer.ADMIN) uuid, data = self.create_user(client) headers, _ = self.do_login(client, data["email"], data["password"]) # Verify custom output fields (if defined) included in the profile response r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) for field in output_fields: assert field in response # Verify custom input fields (if defined) included in the profile input schema r = client.patch(f"{AUTH_URI}/profile", json={"get_schema": 1}, headers=headers) response = self.get_content(r) assert isinstance(response, list) for field in profile_inputs.keys(): for expected in response: if expected["key"] == field: break else: # pragma: no cover pytest.fail( f"Input field {field} not found in profile input schema") # Verify custom registration fields (if defined) included in the reg. schema r = client.post(f"{AUTH_URI}/profile", json={"get_schema": 1}) response = self.get_content(r) assert isinstance(response, list) for field in registration_inputs.keys(): for expected in response: if expected["key"] == field: break else: # pragma: no cover pytest.fail( f"Input field {field} not found in registration input schema" ) headers, _ = self.do_login(client, None, None) # Verify custom admin input fields (if defined) included in admin users schema r = client.post(f"{API_URI}/admin/users", json={"get_schema": 1}, headers=headers) response = self.get_content(r) assert isinstance(response, list) for field in admin_inputs.keys(): for expected in response: if expected["key"] == field: break else: # pragma: no cover pytest.fail( f"Input field {field} not found in admin users input schema" ) # Verify custom admin output fields (if defined) included in admin users output r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) response = self.get_content(r) assert isinstance(response, dict) for field in output_fields: # This will fail assert field in response
def test_sendmail(self, client: FlaskClient, faker: Faker) -> None: headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/mail", headers=headers) assert r.status_code == 405 r = client.put(f"{API_URI}/admin/mail", headers=headers) assert r.status_code == 405 r = client.patch(f"{API_URI}/admin/mail", headers=headers) assert r.status_code == 405 r = client.delete(f"{API_URI}/admin/mail", headers=headers) assert r.status_code == 405 data: Dict[str, Any] = {"dry_run": False} r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["subject"] = faker.pystr() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["body"] = faker.text() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["to"] = faker.pystr() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["to"] = faker.ascii_email() data["body"] = "TEST EMAIL BODY" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 204 mail = self.read_mock_email() body = mail.get("body", "") assert "TEST EMAIL BODY" in body data["dry_run"] = True r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "html_body" in response assert "plain_body" in response assert "subject" in response assert "to" in response assert "cc" in response assert "bcc" in response data["dry_run"] = False data["body"] = "TEST EMAIL <b>HTML</b> BODY" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 204 mail = self.read_mock_email() body = mail.get("body", "") assert "TEST EMAIL <b>HTML</b> BODY" in body data["dry_run"] = True r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "html_body" in response assert "plain_body" in response assert "subject" in response assert "to" in response assert "cc" in response assert "bcc" in response data["dry_run"] = False data["body"] = faker.text() data["cc"] = faker.pystr() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["cc"] = faker.ascii_email() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 204 data["cc"] = f"{faker.ascii_email()},{faker.pystr()}" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["cc"] = f"{faker.ascii_email()},{faker.ascii_email()}" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 204 data["bcc"] = faker.pystr() r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["bcc"] = f"{faker.ascii_email()},{faker.pystr()}" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 400 data["bcc"] = f"{faker.ascii_email()},{faker.ascii_email()}" r = client.post(f"{API_URI}/admin/mail", json=data, headers=headers) assert r.status_code == 204 mail = self.read_mock_email() body = mail.get("body", "") email_headers = mail.get("headers", "") assert body is not None assert email_headers is not None # Subject: is a key in the MIMEText assert f"Subject: {data['subject']}" in email_headers ccs = mail.get("cc", []) assert ccs[0] == data["to"] assert ccs[1] == data["cc"].split(",") assert ccs[2] == data["bcc"].split(",")
def test_GET_specs(self, client: FlaskClient) -> None: r = client.get(f"{API_URI}/tests/pagination") assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 20 assert content[0] == 1 assert content[19] == 20 r = client.get(f"{API_URI}/tests/pagination", query_string={"get_total": True}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 # Check precedence: get_total wins data = {"get_total": True, "page": 1, "size": 20} r = client.get(f"{API_URI}/tests/pagination", query_string=data) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 r = client.get(f"{API_URI}/tests/pagination", query_string={"page": 2}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 20 assert content[0] == 21 assert content[19] == 40 data = {"page": 2, "size": 10} r = client.get(f"{API_URI}/tests/pagination", query_string=data) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 10 assert content[0] == 11 assert content[9] == 20 data = {"page": 2, "size": 100} r = client.get(f"{API_URI}/tests/pagination", query_string=data) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 50 assert content[0] == 101 assert content[49] == 150 r = client.get(f"{API_URI}/tests/pagination", query_string={"page": 20}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 0 r = client.get(f"{API_URI}/tests/pagination", query_string={"size": 101}) assert r.status_code == 400 r = client.get(f"{API_URI}/tests/pagination", query_string={"page": -5}) assert r.status_code == 400 r = client.get(f"{API_URI}/tests/pagination", query_string={"size": -5}) assert r.status_code == 400 data = {"page": -5, "size": -5} r = client.get(f"{API_URI}/tests/pagination", query_string=data) assert r.status_code == 400 r = client.post(f"{API_URI}/tests/pagination", json={}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 20 assert content[0] == 1 assert content[19] == 20 r = client.post(f"{API_URI}/tests/pagination", json={"get_total": True}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 # Check precedence: get_total wins data = {"get_total": True, "page": 1, "size": 20} r = client.post(f"{API_URI}/tests/pagination", json=data) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, int) assert content == 150 r = client.post(f"{API_URI}/tests/pagination", json={"page": 2}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 20 assert content[0] == 21 assert content[19] == 40 r = client.post(f"{API_URI}/tests/pagination", json={ "page": 2, "size": 10 }) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 10 assert content[0] == 11 assert content[9] == 20 r = client.post(f"{API_URI}/tests/pagination", json={ "page": 2, "size": 100 }) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 50 assert content[0] == 101 assert content[49] == 150 r = client.post(f"{API_URI}/tests/pagination", json={"page": 20}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 0 r = client.post(f"{API_URI}/tests/pagination", json={"size": 101}) assert r.status_code == 400 r = client.post(f"{API_URI}/tests/pagination", json={"page": -5}) assert r.status_code == 400 r = client.post(f"{API_URI}/tests/pagination", json={"size": -5}) assert r.status_code == 400 r = client.post(f"{API_URI}/tests/pagination", json={ "page": -5, "size": -5 }) assert r.status_code == 400 # Final check: # get only accept query parameters # post only accept body parameters r = client.get(f"{API_URI}/tests/pagination", json={"get_total": True}) assert r.status_code == 200 content = self.get_content(r) # Request get_total as body parameter but is ignored => sent a list of elements assert isinstance(content, list) # Request get_total as query parameter but is ignored => sent a list of elements r = client.post(f"{API_URI}/tests/pagination", json={}, query_string={"get_total": True}) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list)
def test_02_unlock_token(self, client: FlaskClient) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return uuid, data = self.create_user(client) self.delete_mock_email() for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None auth = Connector.get_authentication_instance() logins = auth.get_logins(data["email"]) login = logins[-1] assert login.username == data["email"] assert login.failed assert not login.flushed logins = auth.get_logins(data["email"], only_unflushed=True) assert len(logins) > 0 # Check if token is valid r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login_unlock.value assert events[0].user == data["email"] assert events[0].target_type == "User" assert events[0].url == f"/auth/login/unlock/{token}" logins = auth.get_logins(data["email"]) login = logins[-1] assert login.username == data["email"] assert login.failed assert login.flushed logins = auth.get_logins(data["email"], only_unflushed=True) assert len(logins) == 0 # Now credentials are unlock again :-) headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None # Unlock token can be used twice r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 # Verify that unlock tokens can't be used if the user is already unlocked for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None time.sleep(ban_duration) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 # Verify that unlock tokens are invalidated by new tokens for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) first_token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None time.sleep(ban_duration) for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) second_token = self.verify_credentials_ban_notification() assert first_token != second_token r = client.post(f"{AUTH_URI}/login/unlock/{first_token}") assert r.status_code == 400 r = client.post(f"{AUTH_URI}/login/unlock/{second_token}") assert r.status_code == 200 # Test invalid tokens # Token created for another user token = self.get_crafted_token("u") r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created with a wrong algorithm token = self.get_crafted_token("u", wrong_algorithm=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created with a wrong secret token = self.get_crafted_token("u", wrong_secret=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created for another user headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) uuid = str(response.get("uuid")) token = self.get_crafted_token("x", user_id=uuid) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("u", user_id=uuid) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Immature token token = self.get_crafted_token("u", user_id=uuid, immature=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Expired token token = self.get_crafted_token("u", user_id=uuid, expired=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token: this request is expired"
def test_07_totp_failures(self, client: FlaskClient, faker: Faker) -> None: uuid, data = self.create_user(client) username = data["email"] password = data["password"] new_password = faker.password(strong=True) invalid_totp = ( str(faker.pyint(min_value=0, max_value=9)), str(faker.pyint(min_value=10, max_value=99)), str(faker.pyint(min_value=100, max_value=999)), str(faker.pyint(min_value=1000, max_value=9999)), str(faker.pyint(min_value=10000, max_value=99999)), str(faker.pyint(min_value=1000000, max_value=9999999)), faker.pystr(6), ) ################################### # Test first password change ################################### data = { "username": username, "password": password, "new_password": new_password, "password_confirm": new_password, } r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 403 resp = self.get_content(r) assert "actions" in resp assert "errors" in resp assert "FIRST LOGIN" in resp["actions"] assert "TOTP" in resp["actions"] assert "Please change your temporary password" in resp["errors"] assert "You do not provided a valid verification code" in resp[ "errors"] # validate that the QR code is a valid PNG image # ... not implemented data["totp_code"] = "000000" r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == username password = new_password ################################### # Test login ################################### data = { "username": username, "password": password, } r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 403 resp = self.get_content(r) assert "actions" in resp assert "errors" in resp assert "TOTP" in resp["actions"] assert "You do not provided a valid verification code" in resp[ "errors"] data["totp_code"] = "000000" r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.post(f"{AUTH_URI}/login", data=data) assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == username ################################### # Test password change ################################### new_password = faker.password(strong=True) headers, _ = self.do_login(client, username, password) data = { "password": password, "new_password": new_password, "password_confirm": new_password, } r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 401 assert self.get_content(r) == "Verification code is missing" data["totp_code"] = "000000" r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 401 assert self.get_content(r) == "Verification code is not valid" events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].user == username assert "totp" in events[0].payload assert events[0].payload["totp"] == OBSCURE_VALUE for totp in invalid_totp: data["totp_code"] = totp r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 resp = self.get_content(r) assert "totp_code" in resp assert "Invalid TOTP format" in resp["totp_code"] data["totp_code"] = self.generate_totp(username) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 # After a change password a spam of delete Token is expected # Reverse the list and skip all delete tokens to find the change pwd event events = self.get_last_events(100) events.reverse() for event in events: if event.event == Events.delete.value: assert event.target_type == "Token" continue assert event.event == Events.change_password.value assert event.user == username break # verify the new password headers, _ = self.do_login(client, username, new_password) assert headers is not None ################################### # Goodbye temporary user ################################### self.delete_user(client, uuid)
def test_api_stats(self, client: FlaskClient, faker: Faker) -> None: # setup the test env ( admin_headers, uuid_group_A, user_A1_uuid, user_A1_headers, uuid_group_B, user_B1_uuid, user_B1_headers, user_B2_uuid, user_B2_headers, study1_uuid, study2_uuid, ) = create_test_env(client, faker, study=True) # create a dataset for group A dataset_A = {"name": faker.pystr(), "description": faker.pystr()} r = client.post( f"{API_URI}/study/{study2_uuid}/datasets", headers=user_A1_headers, data=dataset_A, ) assert r.status_code == 200 dataset_A_uuid = self.get_content(r) assert isinstance(dataset_A_uuid, str) # create a dataset for group B dataset_B = {"name": faker.pystr(), "description": faker.pystr()} r = client.post( f"{API_URI}/study/{study1_uuid}/datasets", headers=user_B1_headers, data=dataset_B, ) assert r.status_code == 200 dataset_B_uuid = self.get_content(r) assert isinstance(dataset_B_uuid, str) # init an upload in dataset A fake_filename = f"{faker.pystr()}_R1" fake_file = { "name": f"{fake_filename}.fastq.gz", "mimeType": "application/gzip", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_A_uuid}/files/upload", headers=user_A1_headers, data=fake_file, ) assert r.status_code == 201 # init an upload in dataset B r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=user_B1_headers, data=fake_file, ) assert r.status_code == 201 # test without group filter # public stats NIGEndpoint.GROUPS_TO_FILTER = [] r = client.get(f"{API_URI}/stats/public", ) assert r.status_code == 200 full_public_stats = self.get_content(r) assert isinstance(full_public_stats, dict) assert full_public_stats["num_users"] > 0 assert full_public_stats["num_studies"] > 0 assert full_public_stats["num_datasets"] > 0 assert full_public_stats["num_files"] > 0 # private stats r = client.get( f"{API_URI}/stats/private", headers=user_B1_headers, ) private_stats = self.get_content(r) assert isinstance(private_stats, dict) assert private_stats["num_users"] > 0 assert private_stats["num_studies"] > 0 assert private_stats["num_datasets"] > 0 assert private_stats["num_files"] > 0 # exclude test group NIGEndpoint.GROUPS_TO_FILTER = ["Default group"] # test public stats r = client.get(f"{API_URI}/stats/public", ) assert r.status_code == 200 public_stats = self.get_content(r) assert isinstance(public_stats, dict) assert public_stats["num_users"] == 3 assert public_stats["num_studies"] == 2 assert public_stats["num_datasets"] == 2 assert public_stats["num_files"] == 2 # test authentication for private stats r = client.get(f"{API_URI}/stats/private", ) assert r.status_code == 401 # test private stats r = client.get( f"{API_URI}/stats/private", headers=user_B1_headers, ) private_stats = self.get_content(r) assert isinstance(private_stats, dict) assert private_stats["num_users"] == 3 assert private_stats["num_studies"] == 2 assert private_stats["num_datasets"] == 2 assert private_stats["num_files"] == 2 # get group fullnames graph = neo4j.get_instance() group_A = graph.Group.nodes.get_or_none(uuid=uuid_group_A) group_A_fullname = group_A.fullname group_B = graph.Group.nodes.get_or_none(uuid=uuid_group_B) group_B_fullname = group_B.fullname assert private_stats["num_datasets_per_group"][group_A_fullname] == 1 assert private_stats["num_datasets_per_group"][group_B_fullname] == 1 # check the excluded group not in responses assert "Default group" not in private_stats["num_datasets_per_group"] # test empty stats NIGEndpoint.GROUPS_TO_FILTER.append(group_A_fullname) NIGEndpoint.GROUPS_TO_FILTER.append(group_B_fullname) # public r = client.get(f"{API_URI}/stats/public", ) assert r.status_code == 200 public_stats = self.get_content(r) assert isinstance(public_stats, dict) assert public_stats["num_users"] == 0 assert public_stats["num_studies"] == 0 assert public_stats["num_datasets"] == 0 assert public_stats["num_files"] == 0 # private r = client.get( f"{API_URI}/stats/private", headers=user_B1_headers, ) private_stats = self.get_content(r) assert isinstance(private_stats, dict) assert private_stats["num_users"] == 0 assert private_stats["num_studies"] == 0 assert private_stats["num_datasets"] == 0 assert private_stats["num_files"] == 0 assert group_A_fullname not in private_stats["num_datasets_per_group"] assert group_B_fullname not in private_stats["num_datasets_per_group"] # delete all the elements used by the test delete_test_env( client, user_A1_headers, user_B1_headers, user_B1_uuid, user_B2_uuid, user_A1_uuid, uuid_group_A, uuid_group_B, study1_uuid=study1_uuid, study2_uuid=study2_uuid, )
def test_chunked(self, client: FlaskClient, faker: Faker) -> None: self.fname = self.get("fname") self.fcontent = self.get("fcontent") r = client.post(f"{API_URI}/tests/upload", data={"force": True}) assert r.status_code == 400 data = { "force": True, "name": "fixed.filename", "size": "999", "mimeType": "application/zip", "lastModified": 1590302749209, } r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 201 assert self.get_content(r) == "" with io.StringIO(faker.text()) as f: r = client.put(f"{API_URI}/tests/upload/chunked", data=f) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" with io.StringIO(faker.text()) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": "!"}, ) assert r.status_code == 400 assert self.get_content(r) == "Invalid request" up_data = faker.pystr(min_chars=24, max_chars=48) STR_LEN = len(up_data) with io.StringIO(up_data[0:5]) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes 0-5/{STR_LEN}"}, ) assert r.status_code == 206 assert self.get_content(r) == "partial" with io.StringIO(up_data[5:]) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes 5-{STR_LEN}/{STR_LEN}"}, ) assert r.status_code == 200 c = self.get_content(r) assert c.get("filename") is not None uploaded_filename = c.get("filename") meta = c.get("meta") assert meta is not None assert meta.get("charset") == "us-ascii" assert meta.get("type") == "text/plain" r = client.get(f"{API_URI}/tests/download/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get(f"{API_URI}/tests/download/{uploaded_filename}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == up_data r = client.get(f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": ""}) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"0-{STR_LEN - 1}"}, ) assert r.status_code == 416 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": "bytes=0-9999999999999999"}, ) from werkzeug import __version__ as werkzeug_version # Back-compatibility check for B2STAGE if werkzeug_version == "0.16.1": # pragma: no cover assert r.status_code == 200 else: assert r.status_code == 206 r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": "bytes=0-4"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[0:5] r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"bytes=5-{STR_LEN - 1}"}, ) assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data[5:] r = client.get( f"{API_URI}/tests/download/{uploaded_filename}", headers={"Range": f"bytes=0-{STR_LEN - 1}"}, ) # Back-compatibility check for B2STAGE if werkzeug_version == "0.16.1": # pragma: no cover assert r.status_code == 200 else: assert r.status_code == 206 content = r.data.decode("utf-8") assert content == up_data # Send a new string as content file. Will be appended as prefix up_data2 = faker.pystr(min_chars=24, max_chars=48) STR_LEN = len(up_data2) with io.StringIO(up_data2) as f: r = client.put( f"{API_URI}/tests/upload/chunked", data=f, headers={"Content-Range": f"bytes */{STR_LEN}"}, ) assert r.status_code == 200 # c = self.get_content(r) # assert c.get('filename') is not None # uploaded_filename = c.get('filename') # meta = c.get('meta') # assert meta is not None # assert meta.get('charset') == 'us-ascii' # assert meta.get('type') == 'text/plain' # r = client.get(f'{API_URI}/tests/download/{uploaded_filename}') # assert r.status_code == 200 # content = r.data.decode('utf-8') # # Uhmmm... should not be up_data2 + up_data ?? # assert content == up_data + up_data2 data["force"] = False r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 400 err = f"File '{uploaded_filename}' already exists" assert self.get_content(r) == err data["force"] = True r = client.post(f"{API_URI}/tests/upload", data=data) assert r.status_code == 201 assert self.get_content(r) == ""
def test_admin_users(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return project_tile = get_project_configuration("project.title", default="YourProject") headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/users", headers=headers) assert r.status_code == 200 schema = self.getDynamicInputSchema(client, "admin/users", headers) data = self.buildData(schema) data["email_notification"] = True data["is_active"] = True data["expiration"] = None # Event 1: create r = client.post(f"{API_URI}/admin/users", data=data, headers=headers) assert r.status_code == 200 uuid = self.get_content(r) mail = self.read_mock_email() # Subject: is a key in the MIMEText assert mail.get("body") is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: new credentials" in mail.get( "headers") assert f"Username: {data.get('email', 'MISSING').lower()}" in mail.get( "body") assert f"Password: {data.get('password')}" in mail.get("body") # Event 2: read r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert len(users_list) > 0 # email is saved lowercase assert users_list[0].get("email") == data.get("email", "MISSING").lower() # Check duplicates r = client.post(f"{API_URI}/admin/users", data=data, headers=headers) assert r.status_code == 409 # Create another user data2 = self.buildData(schema) data2["email_notification"] = True data2["is_active"] = True data2["expiration"] = None # Event 3: create r = client.post(f"{API_URI}/admin/users", data=data2, headers=headers) assert r.status_code == 200 uuid2 = self.get_content(r) mail = self.read_mock_email() # Subject: is a key in the MIMEText assert mail.get("body") is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: new credentials" in mail.get( "headers") assert f"Username: {data2.get('email', 'MISSING').lower()}" in mail.get( "body") assert f"Password: {data2.get('password')}" in mail.get("body") # send and invalid user_id r = client.put( f"{API_URI}/admin/users/invalid", data={"name": faker.name()}, headers=headers, ) assert r.status_code == 404 # Event 4: modify r = client.put( f"{API_URI}/admin/users/{uuid}", data={"name": faker.name()}, headers=headers, ) assert r.status_code == 204 # email cannot be modified new_data = {"email": data.get("email")} r = client.put(f"{API_URI}/admin/users/{uuid2}", data=new_data, headers=headers) # from webargs >= 6 this endpoint no longer return a 204 but a 400 # because email is an unknown field # assert r.status_code == 204 assert r.status_code == 400 # Event 5: read r = client.get(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert len(users_list) > 0 # email is not modified -> still equal to data2, not data1 assert users_list[0].get("email") != data.get("email", "MISSING").lower() assert users_list[0].get("email") == data2.get("email", "MISSING").lower() r = client.delete(f"{API_URI}/admin/users/invalid", headers=headers) assert r.status_code == 404 # Event 6: delete r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 204 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 404 # change password of user2 # Event 7: modify newpwd = faker.password(strong=True) data = {"password": newpwd, "email_notification": True} r = client.put(f"{API_URI}/admin/users/{uuid2}", data=data, headers=headers) assert r.status_code == 204 mail = self.read_mock_email() # Subject: is a key in the MIMEText assert mail.get("body") is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: password changed" in mail.get( "headers") assert f"Username: {data2.get('email', 'MISSING').lower()}" in mail.get( "body") assert f"Password: {newpwd}" in mail.get("body") # login with a newly created user headers2, _ = self.do_login(client, data2.get("email"), newpwd) # normal users cannot access to this endpoint r = client.get(f"{API_URI}/admin/users", headers=headers2) assert r.status_code == 401 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 r = client.post(f"{API_URI}/admin/users", data=data, headers=headers2) assert r.status_code == 401 r = client.put( f"{API_URI}/admin/users/{uuid}", data={"name": faker.name()}, headers=headers2, ) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 # Users are not authorized to /admin/tokens # These two tests should be moved in test_endpoints_tokens.py r = client.get(f"{API_URI}/admin/tokens", headers=headers2) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/tokens/xyz", headers=headers2) assert r.status_code == 401 # let's delete the second user # Event 8: delete r = client.delete(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 204 # Restore the default password, if it changed due to FORCE_FIRST_PASSWORD_CHANGE # or MAX_PASSWORD_VALIDITY errors r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 uuid = self.get_content(r).get("uuid") data = { "password": BaseAuthentication.default_password, # very important, otherwise the default user will lose its admin role "roles": json.dumps(["admin_root"]), } # Event 9: modify r = client.put(f"{API_URI}/admin/users/{uuid}", data=data, headers=headers) assert r.status_code == 204 r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204