From e5eaa6944be01005f23e4d666323b305ece7c7dc Mon Sep 17 00:00:00 2001 From: pdmurray Date: Wed, 6 Mar 2024 15:27:29 -0800 Subject: [PATCH 1/3] Add user journey for logging in and deleting shared environment --- .../tests/user_journeys/test_user_journeys.py | 70 +++++++++-- .../tests/user_journeys/utils/api_utils.py | 112 ++++++++++++++---- 2 files changed, 146 insertions(+), 36 deletions(-) diff --git a/conda-store-server/tests/user_journeys/test_user_journeys.py b/conda-store-server/tests/user_journeys/test_user_journeys.py index 1d4ee7d23..f9ae57fdd 100644 --- a/conda-store-server/tests/user_journeys/test_user_journeys.py +++ b/conda-store-server/tests/user_journeys/test_user_journeys.py @@ -29,15 +29,65 @@ def test_admin_user_can_create_environment( base_url: str, token: str, specification_path: str ) -> None: """Test that an admin user can create an environment.""" - namespace = utils.API.gen_random_namespace() api = utils.API(base_url=base_url, token=token) - api.create_namespace(namespace) - response = api.create_environment(namespace, specification_path) - data = response.json()["data"] - assert "build_id" in data - build_id = data["build_id"] - assert build_id is not None - build = api.wait_for_successful_build(build_id) - environment_name = build.json()["data"]["specification"]["name"] - api.delete_environment(namespace, environment_name) + namespace = "default" + environment = api.create_environment(namespace, specification_path).json()["data"][ + "specification" + ]["name"] + api.delete_environment(namespace, environment) + + +@pytest.mark.user_journey +@pytest.mark.parametrize( + "specification_path", + [ + ("tests/user_journeys/test_data/simple_environment.yaml"), + ], +) +def test_admin_login_and_delete_shared_environment( + base_url: str, specification_path: str +) -> None: + """Test that a user can login and delete an environment in a shared namespace.""" + api = utils.API(base_url=base_url) + + # Create a shared namespace; default permissions for namepace/environment */* is admin + namespace = api.create_namespace().json()["data"]["name"] + environment = api.create_environment( + namespace, + specification_path, + ).json()["data"]["specification"]["name"] + + api.delete_environment(namespace, environment) + api.delete_namespace(namespace) + + +@pytest.mark.user_journey +@pytest.mark.parametrize( + "specification_path", + [ + ("tests/user_journeys/test_data/simple_environment.yaml"), + ], +) +def test_user_login_and_delete_shared_environment( + base_url: str, specification_path: str +) -> None: + """Test that a user can login and delete an environment in a shared namespace.""" + api = utils.API(base_url=base_url) + # Create a shared namespace; default permissions for namepace/environment */* is admin + namespace = api.create_namespace().json()["data"]["name"] + + dev_api = utils.API( + base_url=base_url, + token=api.create_token( + namespace, + 'editor', + ).json()['data']['token'] + ) + + environment = dev_api.create_environment( + namespace, + specification_path, + ).json()["data"]["specification"]["name"] + + dev_api.delete_environment(namespace, environment) api.delete_namespace(namespace) diff --git a/conda-store-server/tests/user_journeys/utils/api_utils.py b/conda-store-server/tests/user_journeys/utils/api_utils.py index 4fcef9035..0a50fd321 100644 --- a/conda-store-server/tests/user_journeys/utils/api_utils.py +++ b/conda-store-server/tests/user_journeys/utils/api_utils.py @@ -2,6 +2,7 @@ import time import uuid from enum import Enum +from typing import Union import requests import utils.time_utils as time_utils @@ -19,6 +20,11 @@ class BuildStatus(Enum): CANCELED = "CANCELED" +class NamespaceStatus(Enum): + OK = "ok" + ERROR = "error" + + class API: """ Helper class for making requests to the API. @@ -69,9 +75,44 @@ def _login(self, username: str, password: str) -> None: data = token_response.json() self.token = data["data"]["token"] - def create_namespace(self, namespace: str) -> requests.Response: - """Create a namespace.""" - return self._make_request(f"api/v1/namespace/{namespace}", method="POST") + def create_namespace( + self, + namespace: Union[str, None] = None, + max_iterations: int = 100, + sleep_time: int = 5, + ) -> requests.Response: + """Create a namespace. + + Parameters + ---------- + namespace : str + Name of the namespace to create. If None, use a random namespace name + max_iterations : int + Max number of times to check whether the namespace was created before failing + sleep_time : int + Seconds to wait between each status check + + Returns + ------- + requests.Response + Response from the conda-store server + """ + if namespace is None: + namespace = self.gen_random_namespace() + + self._make_request(f"api/v1/namespace/{namespace}", method="POST") + for i in range(max_iterations): + response = self._make_request(f"api/v1/namespace/{namespace}") + status = NamespaceStatus(response.json()["status"]) + if status in [NamespaceStatus.OK, NamespaceStatus.ERROR]: + return response + + time.sleep(sleep_time) + + raise TimeoutError( + f"Timed out waiting to create namespace {namespace}. Current response: " + f"{response.json()}" + ) def create_token( self, namespace: str, role: str, default_namespace: str = "default" @@ -85,37 +126,56 @@ def create_token( return self._make_request("api/v1/token", method="POST", json_data=json_data) def create_environment( - self, namespace: str, specification_path: str + self, + namespace: str, + specification_path: str, + max_iterations: int = 100, + sleep_time: int = 5, ) -> requests.Response: - """ - Create an environment. - The environment specification is read - from a conda environment.yaml file. + """Create an environment. + + Parameters + ---------- + namespace : str + Namespace the environment should be written to + specification_path : str + Path to conda environment specification file + max_iterations : int + Max number of times to check whether the build completed before failing + sleep_time : int + Seconds to wait between each status check + + Returns + ------- + requests.Response + Response from the conda-store server """ with open(specification_path, "r", encoding="utf-8") as file: specification_content = file.read() - json_data = {"namespace": namespace, "specification": specification_content} - - return self._make_request( - "api/v1/specification", method="POST", json_data=json_data + response = self._make_request( + "api/v1/specification", + method="POST", + json_data={"namespace": namespace, "specification": specification_content}, ) + build_id = response.json()["data"]["build_id"] + for i in range(max_iterations): + response = self._make_request(f"api/v1/build/{build_id}/") + status = BuildStatus(response.json()["data"]["status"]) + + if status in [ + BuildStatus.COMPLETED, + BuildStatus.FAILED, + BuildStatus.CANCELED, + ]: + return response - def wait_for_successful_build( - self, build_id: str, max_iterations: int = 100, sleep_time: int = 5 - ) -> requests.Response: - """Wait for a build to complete.""" - status = BuildStatus.QUEUED.value - iterations = 0 - while status != BuildStatus.COMPLETED.value: - if iterations > max_iterations: - raise TimeoutError("Timed out waiting for build") - response = self._make_request(f"api/v1/build/{build_id}", method="GET") - status = response.json()["data"]["status"] - assert status != BuildStatus.FAILED.value, "Build failed" - iterations += 1 time.sleep(sleep_time) - return response + + raise TimeoutError( + f"Timed out waiting to create namespace {namespace}. Current response: " + f"{response.json()}" + ) def delete_environment( self, namespace: str, environment_name: str From b15a4e168086bbf4f2fd334a90c08d81ba422ea5 Mon Sep 17 00:00:00 2001 From: pdmurray Date: Thu, 14 Mar 2024 16:28:20 -0700 Subject: [PATCH 2/3] Add another user journey test for a developer creating an env --- .../tests/user_journeys/test_user_journeys.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/conda-store-server/tests/user_journeys/test_user_journeys.py b/conda-store-server/tests/user_journeys/test_user_journeys.py index f9ae57fdd..097fd5241 100644 --- a/conda-store-server/tests/user_journeys/test_user_journeys.py +++ b/conda-store-server/tests/user_journeys/test_user_journeys.py @@ -47,10 +47,11 @@ def test_admin_user_can_create_environment( def test_admin_login_and_delete_shared_environment( base_url: str, specification_path: str ) -> None: - """Test that a user can login and delete an environment in a shared namespace.""" + """Test that an admin can login and create/delete an env in a shared namespace.""" api = utils.API(base_url=base_url) - # Create a shared namespace; default permissions for namepace/environment */* is admin + # Create a shared namespace; default permissions for namepace/environment + # */* is admin namespace = api.create_namespace().json()["data"]["name"] environment = api.create_environment( namespace, @@ -68,19 +69,21 @@ def test_admin_login_and_delete_shared_environment( ("tests/user_journeys/test_data/simple_environment.yaml"), ], ) -def test_user_login_and_delete_shared_environment( +def test_user_login_and_create_shared_environment( base_url: str, specification_path: str ) -> None: - """Test that a user can login and delete an environment in a shared namespace.""" + """Test that a user can login and create an environment in a shared namespace.""" api = utils.API(base_url=base_url) - # Create a shared namespace; default permissions for namepace/environment */* is admin + + # Create a shared namespace; default permissions for namepace/environment + # */* is admin namespace = api.create_namespace().json()["data"]["name"] dev_api = utils.API( base_url=base_url, token=api.create_token( namespace, - 'editor', + 'developer', ).json()['data']['token'] ) @@ -89,5 +92,5 @@ def test_user_login_and_delete_shared_environment( specification_path, ).json()["data"]["specification"]["name"] - dev_api.delete_environment(namespace, environment) + api.delete_environment(namespace, environment) api.delete_namespace(namespace) From c7a789cec3b4eb84ac913e27083a2ef837cc2556 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:13:59 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../tests/user_journeys/test_user_journeys.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/conda-store-server/tests/user_journeys/test_user_journeys.py b/conda-store-server/tests/user_journeys/test_user_journeys.py index 097fd5241..49584a5f0 100644 --- a/conda-store-server/tests/user_journeys/test_user_journeys.py +++ b/conda-store-server/tests/user_journeys/test_user_journeys.py @@ -56,7 +56,9 @@ def test_admin_login_and_delete_shared_environment( environment = api.create_environment( namespace, specification_path, - ).json()["data"]["specification"]["name"] + ).json()["data"][ + "specification" + ]["name"] api.delete_environment(namespace, environment) api.delete_namespace(namespace) @@ -83,14 +85,18 @@ def test_user_login_and_create_shared_environment( base_url=base_url, token=api.create_token( namespace, - 'developer', - ).json()['data']['token'] + "developer", + ).json()[ + "data" + ]["token"], ) environment = dev_api.create_environment( namespace, specification_path, - ).json()["data"]["specification"]["name"] + ).json()[ + "data" + ]["specification"]["name"] api.delete_environment(namespace, environment) api.delete_namespace(namespace)