Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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(",")
Ejemplo n.º 6
0
    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))
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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"
Ejemplo n.º 11
0
    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,
        )
Ejemplo n.º 12
0
    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,
        )
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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,
        )
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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()
Ejemplo n.º 17
0
    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
Ejemplo n.º 18
0
        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
Ejemplo n.º 19
0
    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
Ejemplo n.º 20
0
    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)
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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) == ""
Ejemplo n.º 24
0
    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,
        )
Ejemplo n.º 25
0
    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
Ejemplo n.º 26
0
    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