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_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 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_download(self, client: FlaskClient) -> None: self.fname = self.get("fname") self.fcontent = self.get("fcontent") r = client.get(f"{API_URI}/tests/download/doesnotexist") assert r.status_code == 400 # no filename provided r = client.get(f"{API_URI}/tests/download") assert r.status_code == 400 r = client.get(f"{API_URI}/tests/download/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == self.fcontent new_content = "new content" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(new_content)), self.fname), "force": True, }, ) assert r.status_code == 200 r = client.get(f"{API_URI}/tests/download/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content != self.fcontent assert content == new_content r = client.get(f"{API_URI}/tests/download/{self.fname}", query_string={"stream": True}) assert r.status_code == 200 content = r.data.decode("utf-8") assert content == new_content r = client.get(f"{API_URI}/tests/download/doesnotexist", query_string={"stream": True}) assert r.status_code == 400
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_api_dataset(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 dataset dataset1 = {"name": faker.pystr(), "description": faker.pystr()} r = client.post( f"{API_URI}/study/{study1_uuid}/datasets", headers=user_B1_headers, data=dataset1, ) assert r.status_code == 200 dataset1_uuid = self.get_content(r) assert isinstance(dataset1_uuid, str) # check the directory exists dir_path = INPUT_ROOT.joinpath(uuid_group_B, study1_uuid, dataset1_uuid) assert dir_path.is_dir() # create a new dataset in a study of an other group r = client.post( f"{API_URI}/study/{study2_uuid}/datasets", headers=user_B1_headers, data=dataset1, ) assert r.status_code == 404 # create a technical r = client.post( f"{API_URI}/study/{study1_uuid}/technicals", headers=user_B1_headers, data={"name": faker.pystr()}, ) assert r.status_code == 200 technical_uuid = self.get_content(r) assert isinstance(technical_uuid, str) # create a phenotype r = client.post( f"{API_URI}/study/{study1_uuid}/phenotypes", headers=user_B1_headers, data={ "name": faker.pystr(), "sex": "male" }, ) assert r.status_code == 200 phenotype_uuid = self.get_content(r) assert isinstance(phenotype_uuid, str) # create a new dataset as admin not belonging to study group dataset2 = { "name": faker.pystr(), "description": faker.pystr(), "phenotype": phenotype_uuid, "technical": technical_uuid, } r = client.post( f"{API_URI}/study/{study1_uuid}/datasets", headers=admin_headers, data=dataset2, ) assert r.status_code == 404 r = client.post( f"{API_URI}/study/{study1_uuid}/datasets", headers=user_B1_headers, data=dataset2, ) assert r.status_code == 200 dataset2_uuid = self.get_content(r) assert isinstance(dataset2_uuid, str) # test dataset access # test dataset list response r = client.get(f"{API_URI}/study/{study1_uuid}/datasets", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 2 # test dataset list response for a study you don't have access r = client.get(f"{API_URI}/study/{study2_uuid}/datasets", headers=user_B1_headers) assert r.status_code == 404 # test dataset list response for admin r = client.get(f"{API_URI}/study/{study1_uuid}/datasets", 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 datasets in a study r = client.get(f"{API_URI}/study/{study2_uuid}/datasets", headers=user_A1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert not response # dataset owner r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers) assert r.status_code == 200 # same group of the owner r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B2_headers) assert r.status_code == 200 # dataset owned by an other group r = client.get(f"{API_URI}/dataset/{dataset1_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}/dataset/{dataset1_uuid}", headers=admin_headers) assert r.status_code == 200 # test technical and phenoype assignation when a new dataset is created r = client.get(f"{API_URI}/dataset/{dataset2_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "technical" in response assert "phenotype" in response assert response["technical"]["uuid"] == technical_uuid # check phenotype was correctly assigned assert response["phenotype"]["uuid"] == phenotype_uuid # test dataset changing status r = client.patch( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers, data={"status": "UPLOAD COMPLETED"}, ) assert r.status_code == 204 # check new status in get response r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "status" in response # delete status r = client.patch( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers, data={"status": "-1"}, ) assert r.status_code == 204 # check status has been removed r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert not response["status"] # try to modify a status when the dataset is running graph = neo4j.get_instance() dataset = graph.Dataset.nodes.get_or_none(uuid=dataset1_uuid) dataset.status = "RUNNING" dataset.save() r = client.patch( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers, data={"status": "UPLOAD COMPLETED"}, ) assert r.status_code == 400 # admin tries to modify a status when the dataset is running r = client.patch( f"{API_URI}/dataset/{dataset1_uuid}", headers=admin_headers, data={"status": "UPLOAD COMPLETED"}, ) assert r.status_code == 204 # test dataset modification # modify a dataset you do not own r = client.put( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_A1_headers, data={"description": faker.pystr()}, ) assert r.status_code == 404 # modify a dataset you own r = client.put( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers, data={"description": faker.pystr()}, ) assert r.status_code == 204 # modify a dataset of your group assigning a technical and a phenotype r = client.put( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B2_headers, data={ "name": faker.pystr(), "technical": technical_uuid, "phenotype": phenotype_uuid, }, ) assert r.status_code == 204 # check technical was correctly assigned r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B2_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert "technical" in response assert "phenotype" in response assert response["technical"]["uuid"] == technical_uuid # check phenotype was correctly assigned assert response["phenotype"]["uuid"] == phenotype_uuid # modify a dataset of your group removing a technical and a phenotype r = client.put( f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B2_headers, data={ "technical": "-1", "phenotype": "-1" }, ) assert r.status_code == 204 # check technical was correctly removed r = client.get(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B2_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) assert response["technical"] is None # check phenotype was correctly removed assert response["phenotype"] is None # admin modify a dataset of a group he don't belongs r = client.put( f"{API_URI}/dataset/{dataset1_uuid}", headers=admin_headers, data={"description": faker.pystr()}, ) assert r.status_code == 404 # simulate the dataset has an output directory # create the output directory in the same way is created in launch pipeline task output_path = OUTPUT_ROOT.joinpath(dir_path.relative_to(INPUT_ROOT)) output_path.mkdir(parents=True) assert output_path.is_dir() # delete a dataset # delete a dataset you do not own r = client.delete(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_A1_headers) assert r.status_code == 404 # admin delete a dataset of a group he don't belong r = client.delete(f"{API_URI}/dataset/{dataset1_uuid}", headers=admin_headers) assert r.status_code == 404 # delete a dataset you own r = client.delete(f"{API_URI}/dataset/{dataset1_uuid}", headers=user_B1_headers) assert r.status_code == 204 assert not dir_path.is_dir() # delete a study own by your group r = client.delete(f"{API_URI}/dataset/{dataset2_uuid}", headers=user_B2_headers) assert r.status_code == 204 # check dataset deletion r = client.get(f"{API_URI}/dataset/{dataset1_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 # check directory deletion assert not dir_path.is_dir() assert not output_path.is_dir() # 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_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_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_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_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_api_study(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=False) # create a new study for the group B random_name = faker.pystr() study1 = {"name": random_name, "description": faker.pystr()} r = client.post(f"{API_URI}/study", headers=user_B1_headers, data=study1) assert r.status_code == 200 study1_uuid = self.get_content(r) assert isinstance(study1_uuid, str) # create a new study for the group A random_name2 = faker.pystr() study2 = {"name": random_name2, "description": faker.pystr()} r = client.post(f"{API_URI}/study", headers=user_A1_headers, data=study2) assert r.status_code == 200 study2_uuid = self.get_content(r) assert isinstance(study2_uuid, str) # check the directory was created dir_path = INPUT_ROOT.joinpath(uuid_group_A, study2_uuid) assert dir_path.is_dir() # test study access # test study list response r = client.get(f"{API_URI}/study", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 1 # test admin access r = client.get(f"{API_URI}/study/{study1_uuid}", headers=admin_headers) assert r.status_code == 200 # study owner r = client.get(f"{API_URI}/study/{study1_uuid}", headers=user_B1_headers) assert r.status_code == 200 # other component of the group r = client.get(f"{API_URI}/study/{study1_uuid}", headers=user_B2_headers) assert r.status_code == 200 # study own by an other group r = client.get(f"{API_URI}/study/{study1_uuid}", headers=user_A1_headers) assert r.status_code == 404 not_authorized_message = self.get_content(r) assert isinstance(not_authorized_message, str) # test study modification # modify a study you do not own r = client.put( f"{API_URI}/study/{study1_uuid}", headers=user_A1_headers, data={"description": faker.pystr()}, ) assert r.status_code == 404 # modify a study you own r = client.put( f"{API_URI}/study/{study1_uuid}", headers=user_B1_headers, data={"description": faker.pystr()}, ) assert r.status_code == 204 # delete a study # delete a study you do not own r = client.delete(f"{API_URI}/study/{study1_uuid}", headers=user_A1_headers) assert r.status_code == 404 # delete a study you own # create a new dataset to test if it's deleted with the study dataset = {"name": faker.pystr(), "description": faker.pystr()} r = client.post( f"{API_URI}/study/{study2_uuid}/datasets", headers=user_A1_headers, data=dataset, ) assert r.status_code == 200 dataset_uuid = self.get_content(r) assert isinstance(dataset_uuid, str) dataset_path = dir_path.joinpath(dataset_uuid) assert dataset_path.is_dir() # create a new file to test if it's deleted with the study filename = f"{faker.pystr()}_R1" file_data = { "name": f"{filename}.fastq.gz", "mimeType": "application/gzip", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_uuid}/files/upload", headers=user_A1_headers, data=file_data, ) assert r.status_code == 201 # get the file uuid r = client.get( f"{API_URI}/dataset/{dataset_uuid}/files", headers=user_A1_headers, ) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) file_uuid = file_list[0]["uuid"] # create a new technical to test if it's deleted with the study techmeta = {"name": faker.pystr()} r = client.post( f"{API_URI}/study/{study2_uuid}/technicals", headers=user_A1_headers, data=techmeta, ) assert r.status_code == 200 techmeta_uuid = self.get_content(r) assert isinstance(techmeta_uuid, str) # create a new phenotype to test if it's deleted with the study phenotype = {"name": faker.pystr(), "sex": "male"} r = client.post( f"{API_URI}/study/{study2_uuid}/phenotypes", headers=user_A1_headers, data=phenotype, ) assert r.status_code == 200 phenotype_uuid = self.get_content(r) assert isinstance(phenotype_uuid, str) # simulate the study has an output directory # create the output directory in the same way is created in launch pipeline task output_path = OUTPUT_ROOT.joinpath( dataset_path.relative_to(INPUT_ROOT)) output_path.mkdir(parents=True) assert output_path.is_dir() # delete the study r = client.delete(f"{API_URI}/study/{study2_uuid}", headers=user_A1_headers) assert r.status_code == 204 assert not dir_path.is_dir() assert not dataset_path.is_dir() # check the dataset was deleted r = client.get(f"{API_URI}/dataset/{dataset_uuid}", headers=user_A1_headers) assert r.status_code == 404 # check the file was deleted r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_A1_headers) assert r.status_code == 404 # check the technical was deleted r = client.get(f"{API_URI}/technical/{techmeta_uuid}", headers=user_A1_headers) assert r.status_code == 404 # check the phenotype was deleted r = client.get(f"{API_URI}/phenotype/{phenotype_uuid}", headers=user_A1_headers) assert r.status_code == 404 # check the output dir was deleted assert not output_path.is_dir() # delete a study own by your group r = client.delete(f"{API_URI}/study/{study1_uuid}", headers=user_B2_headers) assert r.status_code == 204 # check study deletion r = client.get(f"{API_URI}/study/{study1_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, )
def test_api_techmeta(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 techmeta techmeta1 = { "name": faker.pystr(), "sequencing_date": faker.date(), "platform": "Other", } r = client.post( f"{API_URI}/study/{study1_uuid}/technicals", headers=user_B1_headers, data=techmeta1, ) assert r.status_code == 200 techmeta1_uuid = self.get_content(r) assert isinstance(techmeta1_uuid, str) # create a new techmeta in a study of an other group r = client.post( f"{API_URI}/study/{study2_uuid}/technicals", headers=user_B1_headers, data=techmeta1, ) assert r.status_code == 404 # create a new technical as admin not belonging to study group techmeta2 = { "name": faker.pystr(), "sequencing_date": faker.date(), "platform": "Other", } r = client.post( f"{API_URI}/study/{study1_uuid}/technicals", headers=admin_headers, data=techmeta2, ) assert r.status_code == 404 r = client.post( f"{API_URI}/study/{study1_uuid}/technicals", headers=user_B1_headers, data=techmeta2, ) assert r.status_code == 200 techmeta2_uuid = self.get_content(r) assert isinstance(techmeta2_uuid, str) # test technical access # test technical list response r = client.get(f"{API_URI}/study/{study1_uuid}/technicals", headers=user_B1_headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert len(response) == 2 # test technical list response for a study you don't have access r = client.get(f"{API_URI}/study/{study2_uuid}/technicals", headers=user_B1_headers) assert r.status_code == 404 # test technical list response for admin r = client.get(f"{API_URI}/study/{study1_uuid}/technicals", 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 technicals in a study r = client.get(f"{API_URI}/study/{study2_uuid}/technicals", 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}/technical/{techmeta1_uuid}", headers=user_B1_headers) assert r.status_code == 200 # same group of the study owner r = client.get(f"{API_URI}/technical/{techmeta1_uuid}", headers=user_B2_headers) assert r.status_code == 200 # technical owned by an other group r = client.get(f"{API_URI}/technical/{techmeta1_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}/technical/{techmeta1_uuid}", headers=admin_headers) assert r.status_code == 200 # test technical modification # modify a non existent technical random_technical = faker.pystr() r = client.put( f"{API_URI}/technical/{random_technical}", headers=user_A1_headers, data={"name": faker.pystr()}, ) assert r.status_code == 404 # modify a technical you do not own r = client.put( f"{API_URI}/technical/{techmeta1_uuid}", headers=user_A1_headers, data={"name": faker.pystr()}, ) assert r.status_code == 404 # modify a technical you own r = client.put( f"{API_URI}/technical/{techmeta1_uuid}", headers=user_B1_headers, data={ "name": faker.pystr(), "sequencing_date": faker.date() }, ) assert r.status_code == 204 # admin modify a technical of a group he don't belongs r = client.put( f"{API_URI}/technical/{techmeta1_uuid}", headers=admin_headers, data={"name": faker.pystr()}, ) assert r.status_code == 404 # delete a technical # delete a technical that does not exists r = client.delete(f"{API_URI}/technical/{random_technical}", headers=user_A1_headers) assert r.status_code == 404 # delete a technical in a study you do not own r = client.delete(f"{API_URI}/technical/{techmeta1_uuid}", headers=user_A1_headers) assert r.status_code == 404 # admin delete a technical of a group he don't belong r = client.delete(f"{API_URI}/technical/{techmeta1_uuid}", headers=admin_headers) assert r.status_code == 404 # delete a technical in a study you own r = client.delete(f"{API_URI}/technical/{techmeta1_uuid}", headers=user_B1_headers) assert r.status_code == 204 # delete a technical in a study own by your group r = client.delete(f"{API_URI}/technical/{techmeta2_uuid}", headers=user_B2_headers) assert r.status_code == 204 # check technical deletion r = client.get(f"{API_URI}/technical/{techmeta1_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_01_login_expiration(self, client: FlaskClient) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return # Let's create a new user with an expiration time of N seconds expiration_time = 10 expiration = datetime.now( pytz.utc) + timedelta(seconds=expiration_time) uuid, data = self.create_user(client, data={"expiration": expiration}) # The user is valid valid_headers, _ = self.do_login(client, data["email"], data["password"]) assert valid_headers is not None # But after N seconds the login will be refused time.sleep(expiration_time) invalid_headers, error = self.do_login( client, data["email"], data["password"], status_code=403, ) assert invalid_headers is None assert error == "Sorry, this account is expired" 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 expired" # This token was valid before the expiration, but should be no longer valid # due to the short TTL set when emitted (capped to expiration time) r = client.get(f"{AUTH_URI}/status", headers=valid_headers) assert r.status_code == 401 if Env.get_bool("ALLOW_PASSWORD_RESET"): reset_data = {"reset_email": data["email"]} r = client.post(f"{AUTH_URI}/reset", data=reset_data) assert r.status_code == 403 assert self.get_content(r) == "Sorry, this account is expired" 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 expired" # Let's extend the account validity for other N seconds admin_headers, _ = self.do_login(client, None, None) expiration = datetime.now( pytz.utc) + timedelta(seconds=expiration_time) r = client.put( f"{API_URI}/admin/users/{uuid}", data={"expiration": expiration}, headers=admin_headers, ) assert r.status_code == 204 # The user is valid again valid_headers, _ = self.do_login(client, data["email"], data["password"]) assert valid_headers is not None # But after N seconds the login will be refused again time.sleep(expiration_time) invalid_headers, error = self.do_login( client, data["email"], data["password"], status_code=403, ) assert invalid_headers is None assert error == "Sorry, this account is expired" 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 expired" # Test reduction of account validity # Let's extent other N seconds admin_headers, _ = self.do_login(client, None, None) expiration = datetime.now( pytz.utc) + timedelta(seconds=expiration_time) r = client.put( f"{API_URI}/admin/users/{uuid}", data={"expiration": expiration}, headers=admin_headers, ) assert r.status_code == 204 # The user is valid again valid_headers, _ = self.do_login(client, data["email"], data["password"]) assert valid_headers is not None # Let's set an already expired date expiration = datetime.now( pytz.utc) - timedelta(seconds=expiration_time) r = client.put( f"{API_URI}/admin/users/{uuid}", data={"expiration": expiration}, headers=admin_headers, ) assert r.status_code == 204 # User is no longer valid invalid_headers, error = self.do_login( client, data["email"], data["password"], status_code=403, ) assert invalid_headers is None assert error == "Sorry, this account is expired" 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 expired" # This token was valid and original TTL was set >= now # But when the user expiration were reduced the token was invalided r = client.get(f"{AUTH_URI}/status", headers=valid_headers) assert r.status_code == 401
def test_api_file(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 dataset 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) # check accesses for post request # upload a new file in a dataset of an other group fake_file = { "name": f"{faker.pystr()}_R1.fastq.gz", "mimeType": "application/gzip", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=user_A1_headers, data=fake_file, ) assert r.status_code == 404 # upload a new file as admin not belonging to study group r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=admin_headers, data=fake_file, ) assert r.status_code == 404 # try to upload a file with a non allowed format fake_format = { "name": f"{faker.pystr()}_R1.txt", "mimeType": "text/plain", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=user_B1_headers, data=fake_format, ) assert r.status_code == 400 # try to upload a file with a wrong nomenclature fake_nomencl_file = { "name": f"{faker.pystr()}.{faker.pystr()}_R1.fastq.gz.{faker.pystr()}", "mimeType": "text/plain", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=user_B1_headers, data=fake_nomencl_file, ) assert r.status_code == 400 fake_nomencl_file2 = { "name": f"{faker.pystr()}.{faker.pystr()}_R1.fastq.gz", "mimeType": "text/plain", "size": faker.pyint(), "lastModified": faker.pyint(), } r = client.post( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload", headers=user_B1_headers, data=fake_nomencl_file2, ) assert r.status_code == 400 valid_fcontent = f"@SEQ_ID\n{faker.pystr(max_chars=12)}\n+{faker.pystr()}\n{faker.pystr(max_chars=12)}" # invalid header, @ is missing invalid_header = f"SEQ_ID\n{faker.pystr(max_chars=12)}\n+{faker.pystr()}\n{faker.pystr(max_chars=12)}" # CASE invalid separator invalid_separator = f"@SEQ_ID\n{faker.pystr(max_chars=12)}\n{faker.pystr()}\n{faker.pystr(max_chars=12)}" # len of sequence != len of quality invalid_sequence = f"@SEQ_ID\n{faker.pystr(max_chars=12)}\n+{faker.pystr()}\n{faker.pystr(max_chars=8)}" # invalid second header invalid_header2 = f"@SEQ_ID\n{faker.pystr(max_chars=12)}\n+{faker.pystr()}\n{faker.pystr(max_chars=12)}\n{faker.pystr()}" # create a file to upload fastq = self.create_fastq_gz(faker, valid_fcontent) # upload a file response = self.upload_file(client, user_B1_headers, fastq, dataset_B_uuid, stream=True) assert response.status_code == 200 # check the file exists and have the expected size filename = fastq.name filesize = fastq.stat().st_size filepath = INPUT_ROOT.joinpath(uuid_group_B, study1_uuid, dataset_B_uuid, filename) assert filepath.is_file() assert filepath.stat().st_size == filesize # upload the same file twice response = self.upload_file(client, user_B2_headers, fastq, dataset_B_uuid, stream=True) assert response.status_code == 409 # upload the same file in a different dataset # create a new dataset dataset_B2 = {"name": faker.pystr(), "description": faker.pystr()} r = client.post( f"{API_URI}/study/{study1_uuid}/datasets", headers=user_B1_headers, data=dataset_B2, ) assert r.status_code == 200 dataset_B2_uuid = self.get_content(r) assert isinstance(dataset_B2_uuid, str) response = self.upload_file(client, user_B2_headers, fastq, dataset_B2_uuid, stream=True) assert response.status_code == 200 # check error if final file size is different from the expected # rename the file to upload fastq2 = fastq.parent.joinpath(f"{faker.pystr()}_R1.fastq.gz") fastq.rename(fastq2) # upload without streaming response = self.upload_file( client, user_B1_headers, fastq2, dataset_B_uuid, stream=False, ) assert response.status_code == 500 error_message = self.get_content(response) assert isinstance(error_message, str) assert ( error_message == "File has not been uploaded correctly: final size does not correspond to total size. Please try a new upload" ) # check uncomplete file has been removed check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, fastq2.name, ) assert not check_filepath.is_file() # check file validation # upload an empty file empty_file = self.create_fastq_gz(faker, "") response = self.upload_file( client, user_B1_headers, empty_file, dataset_B_uuid, stream=True, ) assert response.status_code == 400 # check the empty file has been removed check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, empty_file.name, ) assert not check_filepath.is_file() empty_file.unlink() # upload a file with not valid content # CASE wrong gzip file wrong_gzip = Path(tempfile.gettempdir(), f"{faker.pystr()}_R1.fastq.gz") # Directly write the gz => it is an ascii file and not a valid gz with open(wrong_gzip, "w") as f: f.write(valid_fcontent) response = self.upload_file( client, user_B1_headers, wrong_gzip, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "gzipped" in error_message # check the empty file has been removed check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, wrong_gzip.name, ) assert not check_filepath.is_file() wrong_gzip.unlink() # CASE binary file instead of a text file binary_file = self.create_fastq_gz(faker, faker.binary(), mode="wb") response = self.upload_file( client, user_B1_headers, binary_file, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "binary" in error_message check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, binary_file.name, ) assert not check_filepath.is_file() binary_file.unlink() # CASE invalid header invalid_fastq = self.create_fastq_gz(faker, invalid_header) response = self.upload_file( client, user_B1_headers, invalid_fastq, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "header" in error_message check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, invalid_fastq.name, ) assert not check_filepath.is_file() invalid_fastq.unlink() # CASE invalid separator invalid_fastq = self.create_fastq_gz(faker, invalid_separator) response = self.upload_file( client, user_B1_headers, invalid_fastq, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "separator" in error_message check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, invalid_fastq.name, ) assert not check_filepath.is_file() invalid_fastq.unlink() # CASE invalid sequence line invalid_fastq = self.create_fastq_gz(faker, invalid_sequence) response = self.upload_file( client, user_B1_headers, invalid_fastq, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "lines lengths differ" in error_message check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, invalid_fastq.name, ) assert not check_filepath.is_file() invalid_fastq.unlink() # CASE invalid header for the second read invalid_fastq = self.create_fastq_gz(faker, invalid_header2) response = self.upload_file( client, user_B1_headers, invalid_fastq, dataset_B_uuid, stream=True, ) assert response.status_code == 400 error_message = self.get_content(response) assert isinstance(error_message, str) assert "header" in error_message check_filepath = INPUT_ROOT.joinpath( uuid_group_B, study1_uuid, dataset_B_uuid, invalid_fastq.name, ) assert not check_filepath.is_file() invalid_fastq.unlink() # check accesses on put endpoint # put on a file in a dataset of an other group r = client.put( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload/{filename}", headers=user_A1_headers, ) assert r.status_code == 404 # put a file as admin not belonging to study group r = client.put( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload/{filename}", headers=admin_headers, ) assert r.status_code == 404 # put of a ton existent file r = client.put( f"{API_URI}/dataset/{dataset_B_uuid}/files/upload/{fastq2}.txt.gz", headers=user_B1_headers, ) assert r.status_code == 404 # test file access # test file list response r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=user_B1_headers) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) assert len(file_list) == 1 file_uuid = file_list[0]["uuid"] # test file list response for a dataset you don't have access r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=user_A1_headers) assert r.status_code == 404 # test file list response for admin r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=admin_headers) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) assert len(file_list) == 1 # check use case of file not in the folder # rename the file in the folder as it will not be found temporary_filepath = filepath.with_suffix(".fastq.tmp") filepath.rename(temporary_filepath) r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=user_B1_headers) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) assert file_list[0]["status"] == "unknown" # create an empty file with the original name # test status from unknown to importing filepath.touch() r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=user_B1_headers) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) assert file_list[0]["status"] == "importing" # restore the original file filepath.unlink() r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 file_response = self.get_content(r) assert isinstance(file_response, dict) assert file_response["status"] == "unknown" temporary_filepath.rename(filepath) r = client.get(f"{API_URI}/dataset/{dataset_B_uuid}/files", headers=user_B1_headers) assert r.status_code == 200 file_list = self.get_content(r) assert isinstance(file_list, list) assert file_list[0]["status"] == "uploaded" # dataset owner r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 # same group of the dataset owner r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B2_headers) assert r.status_code == 200 # file owned by an other group r = client.get(f"{API_URI}/file/{file_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}/file/{file_uuid}", headers=admin_headers) assert r.status_code == 200 # check use case of file not in the folder # rename the file in the folder as it will not be found filepath.rename(temporary_filepath) r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 # create an empty file with the original name # test status from unknown to importing filepath.touch() r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 file_res = self.get_content(r) assert isinstance(file_res, dict) assert file_res["status"] == "importing" # restore the original file filepath.unlink() r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 temporary_filepath.rename(filepath) r = client.get(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 200 file_res = self.get_content(r) assert isinstance(file_res, dict) assert file_res["status"] == "uploaded" # delete a file # delete a file that does not exists fake_filename = f"{faker.pystr()}_R1" r = client.delete(f"{API_URI}/file/{fake_filename}", headers=user_A1_headers) assert r.status_code == 404 # delete a file in a dataset you do not own r = client.delete(f"{API_URI}/file/{file_uuid}", headers=user_A1_headers) assert r.status_code == 404 # admin delete a file of a dataset he don't belong r = client.delete(f"{API_URI}/file/{file_uuid}", headers=admin_headers) assert r.status_code == 404 # delete a file in a dataset you own r = client.delete(f"{API_URI}/file/{file_uuid}", headers=user_B1_headers) assert r.status_code == 204 # delete a file in a dataset own by your group r = client.get(f"{API_URI}/dataset/{dataset_B2_uuid}/files", headers=user_B2_headers) file_list = self.get_content(r) assert isinstance(file_list, list) file2_uuid = file_list[0]["uuid"] r = client.delete(f"{API_URI}/file/{file2_uuid}", headers=user_B2_headers) assert r.status_code == 204 # check file deletion r = client.get(f"{API_URI}/file/{file_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 # check physical deletion from the folder assert not filepath.is_file() if fastq.exists(): fastq.unlink() if fastq2.exists(): fastq2.unlink() # 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_group_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 group/users tests") return # Create group 1 with 1 Coordinator and 1 User group1_uuid, _ = self.create_group(client) _, user1_data = self.create_user(client, roles=[Role.COORDINATOR], data={"group": group1_uuid}) _, user2_data = self.create_user(client, roles=[Role.USER], data={"group": group1_uuid}) # Create group 2 with only 1 Coordinator group2_uuid, _ = self.create_group(client) _, user3_data = self.create_user(client, roles=[Role.COORDINATOR], data={"group": group2_uuid}) # Verify POST / PUT and DELETE are not enabled headers, _ = self.do_login(client, user1_data["email"], user1_data["password"]) r = client.post(f"{API_URI}/group/users", headers=headers) assert r.status_code == 405 r = client.put(f"{API_URI}/group/users", headers=headers) assert r.status_code == 405 r = client.delete(f"{API_URI}/group/users", headers=headers) assert r.status_code == 405 r = client.put(f"{API_URI}/group/users/{group1_uuid}", headers=headers) assert r.status_code == 404 r = client.delete(f"{API_URI}/group/users/{group1_uuid}", headers=headers) assert r.status_code == 404 # Verify GET response r = client.get(f"{API_URI}/group/users", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert response is not None assert len(response) == 2 assert "email" in response[0] assert "name" in response[0] assert "surname" in response[0] assert "roles" in response[0] assert "password" not in response[0] assert "uuid" not in response[0] assert "group" not in response[0] assert "belongs_to" not in response[0] assert "first_login" not in response[0] assert "last_login" not in response[0] assert "last_password_change" not in response[0] assert "is_active" not in response[0] assert "privacy_accepted" not in response[0] assert "expiration" not in response[0] email1 = response[0]["email"] email2 = response[1]["email"] assert email1 == user1_data["email"] or email2 == user1_data["email"] assert email1 == user2_data["email"] or email2 == user2_data["email"] assert email1 != user3_data["email"] and email2 != user3_data["email"] # Verify GET response with the other group headers, _ = self.do_login(client, user3_data["email"], user3_data["password"]) r = client.get(f"{API_URI}/group/users", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) assert response is not None assert len(response) == 1 assert "email" in response[0] assert "name" in response[0] assert "surname" in response[0] assert "roles" in response[0] assert "password" not in response[0] assert "uuid" not in response[0] assert "group" not in response[0] assert "belongs_to" not in response[0] assert "first_login" not in response[0] assert "last_login" not in response[0] assert "last_password_change" not in response[0] assert "is_active" not in response[0] assert "privacy_accepted" not in response[0] assert "expiration" not in response[0] assert response[0]["email"] == user3_data["email"] assert response[0]["email"] != user1_data["email"] assert response[0]["email"] != user2_data["email"] # Add an admin to group1 _, user4_data = self.create_user(client, roles=[Role.ADMIN, Role.COORDINATOR], data={"group": group1_uuid}) # Verify as Admin AND Coordinator (Expected: all members, including admins) headers, _ = self.do_login(client, user4_data["email"], user4_data["password"]) r = client.get(f"{API_URI}/group/users", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) members = {r["email"] for r in response} assert len(members) == 3 assert user1_data["email"] in members assert user2_data["email"] in members assert user3_data["email"] not in members assert user4_data["email"] in members # Verify as Coordinator only (Expected: admins to be filtered out) headers, _ = self.do_login(client, user1_data["email"], user1_data["password"]) r = client.get(f"{API_URI}/group/users", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, list) members = {r["email"] for r in response} assert len(members) == 2 assert user1_data["email"] in members assert user2_data["email"] in members assert user3_data["email"] not in members assert user4_data["email"] not in members
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_simple_upload_and_download(self, client: FlaskClient, faker: Faker) -> None: warnings.filterwarnings( "ignore", message="unclosed file <_io.BufferedReader name=") self.fcontent = faker.paragraph() self.save("fcontent", self.fcontent) # as defined in test_upload.py for normal uploads upload_folder = "fixsubfolder" self.fname = f"{faker.pystr()}.notallowed" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), # By setting force False only txt files will be allowed for upload # Strange, but it is how the endpoint is configured to improve the tests "force": False, }, ) assert r.status_code == 400 assert self.get_content(r) == "File extension not allowed" self.fname = f"{faker.pystr()}.txt" self.save("fname", self.fname) r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), # By setting force False only txt files will be allowed for upload # Strange, but it is how the endpoint is configured to improve the tests "force": False, }, ) assert r.status_code == 200 destination_path = DATA_PATH.joinpath(upload_folder, self.fname) assert destination_path.exists() assert oct(os.stat(destination_path).st_mode & 0o777) == "0o440" r = client.put( f"{API_URI}/tests/upload", data={"file": (io.BytesIO(str.encode(self.fcontent)), self.fname)}, ) assert r.status_code == 409 err = f"File '{self.fname}' already exists, use force parameter to overwrite" assert self.get_content(r) == err r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), "force": True, }, ) assert r.status_code == 200 destination_path = DATA_PATH.joinpath(upload_folder, self.fname) assert destination_path.exists() assert oct(os.stat(destination_path).st_mode & 0o777) == "0o440" c = self.get_content(r) assert isinstance(c, dict) assert c.get("filename") == self.fname meta = c.get("meta") assert meta is not None assert meta.get("charset") is not None assert meta.get("type") is not None self.fname = self.get("fname") self.fcontent = self.get("fcontent") # as defined in test_upload.py for normal uploads upload_folder = "fixsubfolder" r = client.get(f"{API_URI}/tests/download/folder/doesnotexist") assert r.status_code == 404 assert self.get_content(r) == "The requested file does not exist" r = client.get( f"{API_URI}/tests/download/{upload_folder}/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content == self.fcontent new_content = "new content" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(new_content)), self.fname), "force": True, }, ) assert r.status_code == 200 r = client.get( f"{API_URI}/tests/download/{upload_folder}/{self.fname}") assert r.status_code == 200 content = r.data.decode("utf-8") assert content != self.fcontent assert content == new_content r = client.get( f"{API_URI}/tests/download/{upload_folder}/{self.fname}", query_string={"stream": True}, ) assert r.status_code == 200 content = r.data.decode("utf-8") assert content == new_content r = client.get( f"{API_URI}/tests/download/{upload_folder}/doesnotexist", query_string={"stream": True}, ) assert r.status_code == 404
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_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_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_03_change_profile(self, client: FlaskClient, faker: Faker) -> None: # Always enable during core tests if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Profile is disabled, skipping tests") return headers, _ = self.do_login(client, None, None) # update profile, no auth r = client.put(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no auth r = client.patch(f"{AUTH_URI}/profile") assert r.status_code == 401 # update profile, no data r = client.patch(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 0 newname = faker.name() newuuid = faker.pystr() r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") is not None assert c.get("name") != newname assert c.get("uuid") is not None assert c.get("uuid") != newuuid # update profile data = {"name": newname, "uuid": newuuid} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) # uuid cannot be modified and will raise an unknown field assert r.status_code == 400 data = {"name": newname} r = client.patch(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 204 events = self.get_last_events(1) assert events[0].event == Events.modify.value assert events[0].user == BaseAuthentication.default_user assert events[0].target_type == "User" # It is true in the core, but projects may introduce additional values # and expand the input dictionary even if initially empty # e.g. meteohub adds here the requests_expiration_days parameter # assert len(events[0].payload) == 1 assert "name" in events[0].payload r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert c.get("name") == newname assert c.get("uuid") != newuuid # change password, no data r = client.put(f"{AUTH_URI}/profile", data={}, headers=headers) assert r.status_code == 400 # Sending a new_password and/or password_confirm without a password newpassword = faker.password() data = {"new_password": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {"new_password": newpassword, "password_confirm": newpassword} r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data = {} data["password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["new_password"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password_confirm"] = faker.password(length=5) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 data["password"] = BaseAuthentication.default_password r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Passwords are too short data["password_confirm"] = data["new_password"] r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 400 # Trying to set new password == password... it is not permitted! data["password_confirm"] = data["password"] data["new_password"] = data["password"] if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) r = client.put(f"{AUTH_URI}/profile", data=data, headers=headers) assert r.status_code == 409 # Change the password data["new_password"] = faker.password(strong=True) data["password_confirm"] = data["new_password"] 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 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 # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, data["new_password"]) # restore the previous password data["password"] = data["new_password"] data["new_password"] = BaseAuthentication.default_password data["password_confirm"] = BaseAuthentication.default_password if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"): data["totp_code"] = BaseTests.generate_totp( BaseAuthentication.default_user) 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 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 # verify the new password headers, _ = self.do_login(client, BaseAuthentication.default_user, BaseAuthentication.default_password) self.save("auth_header", headers)
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
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_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_upload(self, client: FlaskClient, faker: Faker) -> None: self.fcontent = faker.paragraph() self.save("fcontent", self.fcontent) self.fname = f"{faker.pystr()}.notallowed" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), # By setting force False only txt files will be allowed for upload # Strange, but it is how the endpoint is configured to improve the tests "force": False, }, ) assert r.status_code == 400 assert self.get_content(r) == "File extension not allowed" self.fname = f"{faker.pystr()}.not" r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), # By setting force False only txt files will be allowed for upload # Strange, but it is how the endpoint is configured to improve the tests "force": False, }, ) assert r.status_code == 400 assert self.get_content(r) == "File extension not allowed" self.fname = f"{faker.pystr()}.txt" self.save("fname", self.fname) r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), # By setting force False only txt files will be allowed for upload # Strange, but it is how the endpoint is configured to improve the tests "force": False, }, ) assert r.status_code == 200 r = client.put( f"{API_URI}/tests/upload", data={"file": (io.BytesIO(str.encode(self.fcontent)), self.fname)}, ) assert r.status_code == 400 err = f"File '{self.fname}' already exists, use force parameter to overwrite" assert self.get_content(r) == err r = client.put( f"{API_URI}/tests/upload", data={ "file": (io.BytesIO(str.encode(self.fcontent)), self.fname), "force": True, }, ) assert r.status_code == 200 c = self.get_content(r) assert c.get("filename") == self.fname meta = c.get("meta") assert meta is not None assert meta.get("charset") is not None assert meta.get("type") is not None
def test_admin_groups(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return headers, _ = self.do_login(client, None, None) r = client.get(f"{API_URI}/admin/groups", headers=headers) assert r.status_code == 200 schema = self.getDynamicInputSchema(client, "admin/groups", headers) data = self.buildData(schema) # Event 1: create r = client.post(f"{API_URI}/admin/groups", data=data, headers=headers) assert r.status_code == 200 uuid = self.get_content(r) r = client.get(f"{API_URI}/admin/groups", headers=headers) assert r.status_code == 200 groups = self.get_content(r) assert groups assert len(groups) > 0 fullname = None for g in groups: if g.get("uuid") == uuid: fullname = g.get("fullname") break else: # pragma: no cover pytest.fail("Group not found") assert fullname is not None newdata = { "shortname": faker.company(), "fullname": faker.company(), } # Event 2: modify r = client.put(f"{API_URI}/admin/groups/{uuid}", data=newdata, headers=headers) assert r.status_code == 204 r = client.get(f"{API_URI}/admin/groups", headers=headers) assert r.status_code == 200 groups = self.get_content(r) for g in groups: if g.get("uuid") == uuid: assert g.get("fullname") == newdata.get("fullname") assert g.get("fullname") != data.get("fullname") assert g.get("fullname") != fullname r = client.put(f"{API_URI}/admin/groups/xyz", data=data, headers=headers) assert r.status_code == 404 # Event 3: delete r = client.delete(f"{API_URI}/admin/groups/{uuid}", headers=headers) assert r.status_code == 204 r = client.get(f"{API_URI}/admin/groups", headers=headers) assert r.status_code == 200 groups = self.get_content(r) for g in groups: if g.get("uuid") == uuid: # pragma: no cover pytest.fail("Group not deleted!") r = client.delete(f"{API_URI}/admin/groups/xyz", headers=headers) assert r.status_code == 404 # Create a group and assign it to the main user # Profile and AdminUsers will react to this change # Very important: admin_groups must be tested before admin_users and profile r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 user_uuid = self.get_content(r).get("uuid") data = { "fullname": "Default group", "shortname": faker.company(), } # Event 4: create uuid, _ = self.create_group(client, data=data) data = { "group": uuid, # very important, otherwise the default user will lose its admin role "roles": json.dumps(["admin_root"]), } headers, _ = self.do_login(client, None, None) # Event 5: modify r = client.put(f"{API_URI}/admin/users/{user_uuid}", data=data, headers=headers) assert r.status_code == 204