From ebb60498922cd39a72e4dbb1a9d7d0214899c4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Mon, 4 Sep 2023 16:15:05 +0200 Subject: [PATCH] Add more tests for workflow modes #45 --- app/api/dependencies.py | 2 +- app/api/endpoints/workflow.py | 14 +- app/api/endpoints/workflow_execution.py | 2 +- app/api/endpoints/workflow_version.py | 2 +- app/schemas/workflow.py | 2 +- app/schemas/workflow_version.py | 2 +- app/tests/api/test_workflow.py | 320 ++++++++++++++++++++++- app/tests/api/test_workflow_execution.py | 78 +++++- app/tests/api/test_workflow_mode.py | 6 +- app/tests/api/test_workflow_version.py | 185 +++++++++++-- 10 files changed, 569 insertions(+), 44 deletions(-) diff --git a/app/api/dependencies.py b/app/api/dependencies.py index b94a488..eb8d75a 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -255,7 +255,7 @@ async def get_current_workflow_version( git_commit_hash: str = Path( ..., description="Git commit git_commit_hash of specific version.", - regex=r"^([0-9a-f]{40}|latest)$", + pattern=r"^([0-9a-f]{40}|latest)$", examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], ), ) -> WorkflowVersion: diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 8735fa2..ddad1ec 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -1,7 +1,7 @@ from typing import Annotated, Any, Awaitable, Callable, List, Optional, Set from uuid import UUID -from clowmdb.models import Workflow, WorkflowVersion +from clowmdb.models import Workflow, WorkflowMode, WorkflowVersion from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Response, status from app.api.dependencies import AuthorizationDependency, CurrentUser, CurrentWorkflow, DBSession, HTTPClient, S3Service @@ -361,12 +361,10 @@ async def update_workflow( detail=f"Workflow Version with git_commit_hash'{version_update.git_commit_hash}' already exists", ) # Get previous version - previous_version = await CRUDWorkflowVersion.get_latest(db, workflow.workflow_id, published=False) + previous_version: WorkflowVersion = await CRUDWorkflowVersion.get_latest(db, workflow.workflow_id, published=False) # Get modes of previous version - previous_version_modes = await CRUDWorkflowMode.list_modes( - db, previous_version.workflow_modes # type: ignore[union-attr] - ) + previous_version_modes = await CRUDWorkflowMode.list_modes(db, previous_version.git_commit_hash) if version_update.delete_modes is not None: # Check if mode to delete actually exist @@ -375,7 +373,7 @@ async def update_workflow( if delete_mode not in mode_ids: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Workflow mode {delete_mode} does not exist for the latest version of workflow {workflow.workflow_id}", # noqa: E501 + detail=f"Workflow mode {delete_mode} does not exist for the latest version {previous_version.git_commit_hash} of workflow {workflow.workflow_id}", # noqa: E501 ) # Filter out modes that should be deleted in new workflow version previous_version_modes = [ @@ -385,13 +383,13 @@ async def update_workflow( # Build a git repository object based on the repository url repo = build_repository(workflow.repository_url, version_update.git_commit_hash) - check_repo_modes = previous_version_modes + check_repo_modes = previous_version_modes.copy() # If there are new modes, add them to the list for file checking if version_update.append_modes is not None: check_repo_modes += version_update.append_modes await check_repo(repo=repo, client=client, modes=check_repo_modes) - append_modes_db = [] + append_modes_db: List[WorkflowMode] = [] # Create new modes in database if version_update.append_modes is not None: append_modes_db = await CRUDWorkflowMode.create(db, version_update.append_modes) diff --git a/app/api/endpoints/workflow_execution.py b/app/api/endpoints/workflow_execution.py index ca3c30b..9052e0c 100644 --- a/app/api/endpoints/workflow_execution.py +++ b/app/api/endpoints/workflow_execution.py @@ -295,7 +295,7 @@ async def list_workflow_executions( None, description="Filter for workflow version", examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], - regex=r"^[0-9a-f]{40}$", + pattern=r"^[0-9a-f]{40}$", ), ) -> List[WorkflowExecutionOut]: """ diff --git a/app/api/endpoints/workflow_version.py b/app/api/endpoints/workflow_version.py index e910f94..6d75cab 100644 --- a/app/api/endpoints/workflow_version.py +++ b/app/api/endpoints/workflow_version.py @@ -108,7 +108,7 @@ async def get_workflow_version( git_commit_hash: str = Path( ..., description="Git commit git_commit_hash of specific version or 'latest'.", - regex=r"^([0-9a-f]{40}|latest)$", + pattern=r"^([0-9a-f]{40}|latest)$", examples=["latest", "ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], ), ) -> WorkflowVersionSchema: diff --git a/app/schemas/workflow.py b/app/schemas/workflow.py index 17f2e0a..9359b71 100644 --- a/app/schemas/workflow.py +++ b/app/schemas/workflow.py @@ -138,7 +138,7 @@ class WorkflowUpdate(BaseModel): ..., description="Version of the Workflow. Should follow semantic versioning", examples=["v1.1.0"], - minlength=5, + min_length=5, max_length=10, ) git_commit_hash: str = Field( diff --git a/app/schemas/workflow_version.py b/app/schemas/workflow_version.py index cad1201..0d5615a 100644 --- a/app/schemas/workflow_version.py +++ b/app/schemas/workflow_version.py @@ -20,7 +20,7 @@ class WorkflowVersion(WorkflowVersionStatus): ..., description="Version of the Workflow. Should follow semantic versioning", examples=["v1.0.0"], - minlength=5, + min_length=5, max_length=10, ) git_commit_hash: str = Field( diff --git a/app/tests/api/test_workflow.py b/app/tests/api/test_workflow.py index cc5b51c..47058d4 100644 --- a/app/tests/api/test_workflow.py +++ b/app/tests/api/test_workflow.py @@ -415,7 +415,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): mock_s3_service: MockS3ServiceResource, ) -> None: """ - Exhaustive Test for successfully creating a workflow. + Exhaustive Test for successfully creating a workflow with a workflow mode. Parameters ---------- @@ -660,6 +660,83 @@ class TestWorkflowRoutesDelete(_TestWorkflowRoutes): schema_file = random_workflow.versions[0].git_commit_hash + ".json" assert schema_file not in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + @pytest.mark.asyncio + async def test_delete_private_workflow( + self, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_private_workflow: WorkflowOut, + mock_s3_service: MockS3ServiceResource, + ) -> None: + """ + Test for deleting a workflow by its id. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_private_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + """ + response = await client.delete( + "/".join([self.base_path, str(random_private_workflow.workflow_id)]), + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + icon_slug = str(random_private_workflow.versions[0].icon_url).split("/")[-1] + assert icon_slug not in mock_s3_service.Bucket(settings.ICON_BUCKET).objects.all_keys() + schema_file = random_private_workflow.versions[0].git_commit_hash + ".json" + assert schema_file not in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + scm_file = f"{random_private_workflow.workflow_id.hex}.scm" + assert scm_file not in mock_s3_service.Bucket(settings.PARAMS_BUCKET).objects.all_keys() + + @pytest.mark.asyncio + async def test_delete_workflow_with_mode( + self, + client: AsyncClient, + db: AsyncSession, + random_user: UserWithAuthHeader, + random_workflow: WorkflowOut, + random_workflow_mode: WorkflowMode, + mock_s3_service: MockS3ServiceResource, + ) -> None: + """ + Test for deleting a workflow by its id. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + random_workflow_mode : clowmdb.model.WorkflowMode + Random workflow mode for testing + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + """ + response = await client.delete( + "/".join([self.base_path, str(random_workflow.workflow_id)]), + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + icon_slug = str(random_workflow.versions[0].icon_url).split("/")[-1] + assert icon_slug not in mock_s3_service.Bucket(settings.ICON_BUCKET).objects.all_keys() + schema_file = f"{random_workflow.versions[0].git_commit_hash}-{random_workflow_mode.mode_id.hex}.json" + assert schema_file not in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + + mode_db = await db.scalar( + select(WorkflowMode).where(WorkflowMode._mode_id == random_workflow_mode.mode_id.bytes) + ) + assert mode_db is None + class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): @pytest.mark.asyncio @@ -677,6 +754,8 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. """ git_commit_hash = random_hex_string() version_update = WorkflowUpdate( @@ -699,6 +778,241 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): assert db_version is not None assert db_version.status == WorkflowVersion.Status.CREATED + @pytest.mark.asyncio + async def test_update_workflow_with_append_modes( + self, + db: AsyncSession, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow: WorkflowOut, + random_workflow_mode: WorkflowMode, + mock_s3_service: MockS3ServiceResource, + ) -> None: + """ + Test for successfully updating a workflow and adding new modes. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + random_workflow_mode : clowmdb.model.WorkflowMode + Random workflow mode for testing + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + """ + git_commit_hash = random_hex_string() + version_update = WorkflowUpdate( + git_commit_hash=git_commit_hash, + version=random_lower_string(8), + append_modes=[ + WorkflowModeIn( + name=random_lower_string(10), entrypoint=random_lower_string(16), schema_path=random_lower_string() + ) + ], + ).model_dump() + response = await client.post( + "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), + json=version_update, + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_201_CREATED + created_version = response.json() + assert created_version["git_commit_hash"] == git_commit_hash + assert created_version["status"] == WorkflowVersion.Status.CREATED + assert created_version["icon_url"] == str(random_workflow.versions[0].icon_url) + assert created_version["modes"] is not None + assert len(created_version["modes"]) == 2 + + stmt = select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == git_commit_hash) + db_version = await db.scalar(stmt) + assert db_version is not None + assert db_version.status == WorkflowVersion.Status.CREATED + + new_mode_id = next((UUID(m) for m in created_version["modes"] if m != str(random_workflow_mode.mode_id)), None) + assert new_mode_id is not None + assert ( + f"{git_commit_hash}-{new_mode_id.hex}.json" + in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + ) + assert ( + f"{git_commit_hash}-{random_workflow_mode.mode_id.hex}.json" + in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + ) + + # Clean up after test + await db.execute(delete(WorkflowMode).where(WorkflowMode._mode_id == new_mode_id.bytes)) + await db.commit() + mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).Object(f"{git_commit_hash}-{new_mode_id.hex}.json").delete() + + @pytest.mark.asyncio + async def test_update_workflow_with_delete_non_existing_modes( + self, + db: AsyncSession, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow: WorkflowOut, + ) -> None: + """ + Test for updating a workflow and delete an non-existing mode. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + """ + git_commit_hash = random_hex_string() + version_update = WorkflowUpdate( + git_commit_hash=git_commit_hash, version=random_lower_string(8), delete_modes=[str(uuid4())] + ).model_dump_json() + response = await client.post( + "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), + data=version_update, # type: ignore[arg-type] + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.asyncio + async def test_update_workflow_with_delete_modes( + self, + db: AsyncSession, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow: WorkflowOut, + random_workflow_mode: WorkflowMode, + mock_s3_service: MockS3ServiceResource, + ) -> None: + """ + Test for successfully updating a workflow and delete an old mode. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + random_workflow_mode : clowmdb.model.WorkflowMode + Random workflow mode for testing + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + """ + git_commit_hash = random_hex_string() + version_update = WorkflowUpdate( + git_commit_hash=git_commit_hash, + version=random_lower_string(8), + delete_modes=[str(random_workflow_mode.mode_id)], + ).model_dump_json() + response = await client.post( + "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), + data=version_update, # type: ignore[arg-type] + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_201_CREATED + created_version = response.json() + assert created_version["git_commit_hash"] == git_commit_hash + assert created_version["status"] == WorkflowVersion.Status.CREATED + assert created_version["icon_url"] == str(random_workflow.versions[0].icon_url) + assert created_version["modes"] is not None + assert created_version["modes"] is None or len(created_version["modes"]) == 0 + + stmt = select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == git_commit_hash) + db_version = await db.scalar(stmt) + assert db_version is not None + assert db_version.status == WorkflowVersion.Status.CREATED + + assert ( + f"{git_commit_hash}-{random_workflow_mode.mode_id.hex}.json" + not in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + ) + + @pytest.mark.asyncio + async def test_update_workflow_with_append_and_delete_modes( + self, + db: AsyncSession, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow: WorkflowOut, + random_workflow_mode: WorkflowMode, + mock_s3_service: MockS3ServiceResource, + ) -> None: + """ + Test for successfully updating a workflow with adding a new mode and delete an old mode. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. + random_workflow_mode : clowmdb.model.WorkflowMode + Random workflow mode for testing + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + """ + git_commit_hash = random_hex_string() + version_update = WorkflowUpdate( + git_commit_hash=git_commit_hash, + version=random_lower_string(8), + delete_modes=[str(random_workflow_mode.mode_id)], + append_modes=[ + WorkflowModeIn( + name=random_lower_string(10), entrypoint=random_lower_string(16), schema_path=random_lower_string() + ) + ], + ).model_dump_json() + response = await client.post( + "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), + data=version_update, # type: ignore[arg-type] + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_201_CREATED + created_version = response.json() + assert created_version["git_commit_hash"] == git_commit_hash + assert created_version["status"] == WorkflowVersion.Status.CREATED + assert created_version["icon_url"] == str(random_workflow.versions[0].icon_url) + assert created_version["modes"] is not None + assert len(created_version["modes"]) == 1 + assert created_version["modes"][0] != str(random_workflow_mode.mode_id) + + stmt = select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == git_commit_hash) + db_version = await db.scalar(stmt) + assert db_version is not None + assert db_version.status == WorkflowVersion.Status.CREATED + + mode_id = UUID(created_version["modes"][0]) + assert ( + f"{git_commit_hash}-{mode_id.hex}.json" + in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + ) + assert ( + f"{git_commit_hash}-{random_workflow_mode.mode_id.hex}.json" + not in mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).objects.all_keys() + ) + + # Clean up after test + await db.execute(delete(WorkflowMode).where(WorkflowMode._mode_id == mode_id.bytes)) + await db.commit() + mock_s3_service.Bucket(settings.WORKFLOW_BUCKET).Object(f"{git_commit_hash}-{mode_id.hex}.json").delete() + @pytest.mark.asyncio async def test_update_workflow_with_error( self, db: AsyncSession, client: AsyncClient, random_user: UserWithAuthHeader, random_workflow: WorkflowOut @@ -714,6 +1028,8 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. """ version_update = WorkflowUpdate( git_commit_hash=random_hex_string(), @@ -774,6 +1090,8 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): HTTP Client to perform the request on. random_second_user : app.tests.utils.user.UserWithAuthHeader Random second user for testing. + random_workflow : app.schemas.workflow.WorkflowOut + Random workflow for testing. """ version_update = WorkflowUpdate( git_commit_hash=random_hex_string(), diff --git a/app/tests/api/test_workflow_execution.py b/app/tests/api/test_workflow_execution.py index c66ebc7..b310215 100644 --- a/app/tests/api/test_workflow_execution.py +++ b/app/tests/api/test_workflow_execution.py @@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.schemas.workflow import WorkflowOut from app.schemas.workflow_execution import DevWorkflowExecutionIn, WorkflowExecutionIn +from app.schemas.workflow_mode import WorkflowModeIn from app.scm import SCM from app.tests.mocks import MockS3ServiceResource, MockSlurmCluster from app.tests.utils.bucket import add_permission_for_bucket @@ -34,7 +35,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_slurm_cluster: MockSlurmCluster, ) -> None: """ - Test for starting a workflow execution. + Test for starting a workflow execution from a public GitHub repository. Parameters ---------- @@ -148,11 +149,12 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_slurm_cluster: MockSlurmCluster, ) -> None: """ - Test for starting a workflow execution. + Test for starting a workflow execution from a public GitLab repository. Parameters ---------- - db : + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. client : httpx.AsyncClient HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader @@ -262,7 +264,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): random_workflow_version: WorkflowVersion, ) -> None: """ - Test for starting a workflow execution with an unknown workflow version. + Test for starting a workflow execution with a deprecated workflow version. Parameters ---------- @@ -296,7 +298,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): random_workflow_version: WorkflowVersion, ) -> None: """ - Test for starting a workflow execution with a non-existing bucket. + Test for starting a workflow execution with a non-existing bucket in the parameters. Parameters ---------- @@ -323,7 +325,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): random_workflow_version: WorkflowVersion, ) -> None: """ - Test for starting a workflow execution with an existing bucket. + Test for starting a workflow execution with an existing bucket in the parameters. Parameters ---------- @@ -353,7 +355,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): random_workflow_version: WorkflowVersion, ) -> None: """ - Test for starting a workflow execution with a permission for the reference bucket. + Test for starting a workflow execution with a permission for the referenced bucket. Parameters ---------- @@ -387,7 +389,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): random_workflow_version: WorkflowVersion, ) -> None: """ - Test for starting a workflow execution without a permission for the reference bucket. + Test for starting a workflow execution without a permission for the referenced bucket. Parameters ---------- @@ -489,7 +491,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_slurm_cluster: MockSlurmCluster, ) -> None: """ - Test for starting a workflow execution where the report output bucket is a non-existing bucket. + Test for starting a workflow execution with an workflow mode Parameters ---------- @@ -529,7 +531,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_slurm_cluster: MockSlurmCluster, ) -> None: """ - Test for starting a workflow execution where the report output bucket is a non-existing bucket. + Test for starting a workflow execution with a non-existing workflow mode Parameters ---------- @@ -552,6 +554,62 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): + @pytest.mark.asyncio + async def test_start_dev_workflow_execution_from_github_with_mode( + self, + client: AsyncClient, + random_user: UserWithAuthHeader, + mock_s3_service: MockS3ServiceResource, + mock_slurm_cluster: MockSlurmCluster, + ) -> None: + """ + Test for starting a workflow execution with an arbitrary GitHub repository and a workflow mode. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + mock_slurm_cluster : app.tests.mocks.mock_slurm_cluster.MockSlurmCluster + Mock Slurm cluster to inspect submitted jobs. + """ + mode = WorkflowModeIn( + name=random_lower_string(10), entrypoint=random_lower_string(16), schema_path=random_lower_string(16) + ) + execution_in = DevWorkflowExecutionIn( + git_commit_hash=random_hex_string(), + repository_url="https://github.com/example-user/example", + parameters={}, + mode=mode, + ) + response = await client.post( + f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.model_dump() + ) + assert response.status_code == status.HTTP_201_CREATED + execution_response = response.json() + assert execution_response["user_id"] == random_user.user.uid + assert execution_response["status"] == WorkflowExecution.WorkflowExecutionStatus.PENDING + + assert ( + f"params-{UUID(hex=execution_response['execution_id']).hex }.json" + in mock_s3_service.Bucket(settings.PARAMS_BUCKET).objects.all_keys() + ) + + execution_id = UUID(execution_response["execution_id"]) + job = mock_slurm_cluster.get_job_by_name(str(execution_id)) + assert job is not None + assert job["job"]["environment"]["TOWER_WORKSPACE_ID"] == execution_id.hex[:16] + assert "NXF_SCM_FILE" not in job["job"]["environment"].keys() + + nextflow_script = job["script"] + assert "-hub github" in nextflow_script + assert f"-entry {mode.entrypoint}" in nextflow_script + assert f"-revision {execution_in.git_commit_hash}" in nextflow_script + assert f"run {execution_in.repository_url}" in nextflow_script + @pytest.mark.asyncio async def test_start_dev_workflow_execution_from_github( self, diff --git a/app/tests/api/test_workflow_mode.py b/app/tests/api/test_workflow_mode.py index 4b98d5e..be506d5 100644 --- a/app/tests/api/test_workflow_mode.py +++ b/app/tests/api/test_workflow_mode.py @@ -9,13 +9,15 @@ from app.tests.utils.user import UserWithAuthHeader class TestWorkflowModeRoutesGet: + _base_path = "workflow_modes" + @pytest.mark.asyncio async def test_get_non_existing_workflow_mode( self, client: AsyncClient, random_user: UserWithAuthHeader, ) -> None: - response = await client.get(f"/{uuid4()}", headers=random_user.auth_headers) + response = await client.get(f"{self._base_path}/{uuid4()}", headers=random_user.auth_headers) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio @@ -26,7 +28,7 @@ class TestWorkflowModeRoutesGet: random_workflow_mode: WorkflowMode, ) -> None: response = await client.get( - f"workflow_modes/{str(random_workflow_mode.mode_id)}", headers=random_user.auth_headers + f"{self._base_path}/{str(random_workflow_mode.mode_id)}", headers=random_user.auth_headers ) assert response.status_code == status.HTTP_200_OK mode = response.json() diff --git a/app/tests/api/test_workflow_version.py b/app/tests/api/test_workflow_version.py index c403eff..c601d70 100644 --- a/app/tests/api/test_workflow_version.py +++ b/app/tests/api/test_workflow_version.py @@ -1,10 +1,10 @@ from io import BytesIO +from uuid import uuid4 import pytest -from clowmdb.models import WorkflowVersion +from clowmdb.models import WorkflowMode, WorkflowVersion from fastapi import status from httpx import AsyncClient -from mocks.mock_s3_resource import MockS3ServiceResource from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -12,6 +12,7 @@ from app.api.endpoints.workflow_version import DocumentationEnum from app.core.config import settings from app.schemas.workflow import WorkflowOut from app.schemas.workflow_version import WorkflowVersionStatus +from app.tests.mocks.mock_s3_resource import MockS3ServiceResource from app.tests.utils.user import UserWithAuthHeader from app.tests.utils.utils import random_hex_string @@ -227,7 +228,10 @@ class TestWorkflowVersionRoutesUpdate(_TestWorkflowVersionRoutes): class TestWorkflowVersionRoutesGetDocumentation(_TestWorkflowVersionRoutes): @pytest.mark.asyncio async def test_download_workflow_version_documentation( - self, client: AsyncClient, random_user: UserWithAuthHeader, random_workflow: WorkflowOut + self, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow_version: WorkflowVersion, ) -> None: """ Test downloading all the different documentation file for a workflow version. @@ -238,17 +242,17 @@ class TestWorkflowVersionRoutesGetDocumentation(_TestWorkflowVersionRoutes): HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. - random_workflow : app.schemas.workflow.WorkflowOut - Random workflow for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. """ for document in DocumentationEnum: response = await client.get( "/".join( [ self.base_path, - str(random_workflow.workflow_id), + str(random_workflow_version.workflow_id), "versions", - random_workflow.versions[0].git_commit_hash, + random_workflow_version.git_commit_hash, "documentation", ] ), @@ -257,6 +261,79 @@ class TestWorkflowVersionRoutesGetDocumentation(_TestWorkflowVersionRoutes): ) assert response.status_code == status.HTTP_200_OK + @pytest.mark.asyncio + async def test_download_workflow_version_documentation_with_non_existing_mode( + self, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow_version: WorkflowVersion, + ) -> None: + """ + Test downloading all the different documentation file for a workflow version with a non-existing workflow mode. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. + """ + for document in DocumentationEnum: + response = await client.get( + "/".join( + [ + self.base_path, + str(random_workflow_version.workflow_id), + "versions", + random_workflow_version.git_commit_hash, + "documentation", + ] + ), + headers=random_user.auth_headers, + params={"document": document.value, "mode_id": str(uuid4())}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.asyncio + async def test_download_workflow_version_documentation_with_existing_mode( + self, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_workflow_version: WorkflowVersion, + random_workflow_mode: WorkflowMode, + ) -> None: + """ + Test downloading all the different documentation file for a workflow version with a workflow mode. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. + random_workflow_mode : clowmdb.models.WorkflowMode + Random workflow mode for testing. + """ + for document in DocumentationEnum: + response = await client.get( + "/".join( + [ + self.base_path, + str(random_workflow_version.workflow_id), + "versions", + random_workflow_version.git_commit_hash, + "documentation", + ] + ), + headers=random_user.auth_headers, + params={"document": document.value, "mode_id": str(random_workflow_mode.mode_id)}, + ) + assert response.status_code == status.HTTP_200_OK + class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): @pytest.mark.asyncio @@ -264,7 +341,7 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): self, client: AsyncClient, random_user: UserWithAuthHeader, - random_workflow: WorkflowOut, + random_workflow_version: WorkflowVersion, mock_s3_service: MockS3ServiceResource, db: AsyncSession, ) -> None: @@ -277,22 +354,21 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. - random_workflow : app.schemas.workflow.WorkflowOut - Random workflow for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource Mock S3 Service to manipulate objects. db : sqlalchemy.ext.asyncio.AsyncSession. Async database session to perform query on. """ files = {"icon": ("RickRoll.txt", BytesIO(b"Never gonna give you up"), "plain/text")} - version_hash = random_workflow.versions[0].git_commit_hash response = await client.post( "/".join( [ self.base_path, - str(random_workflow.workflow_id), + str(random_workflow_version.workflow_id), "versions", - version_hash, + random_workflow_version.git_commit_hash, "icon", ] ), @@ -302,19 +378,55 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): assert response.status_code == status.HTTP_201_CREATED icon_slug = response.json() assert icon_slug in mock_s3_service.Bucket(settings.ICON_BUCKET).objects.all_keys() - db_version = await db.scalar(select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == version_hash)) + db_version = await db.scalar( + select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == random_workflow_version.git_commit_hash) + ) assert db_version is not None assert db_version.icon_slug == icon_slug # Clean up mock_s3_service.Bucket(settings.ICON_BUCKET).Object(icon_slug).delete() + @pytest.mark.asyncio + async def test_upload_new_icon_as_non_developer( + self, + client: AsyncClient, + random_second_user: UserWithAuthHeader, + random_workflow_version: WorkflowVersion, + ) -> None: + """ + Test for uploading a new icon for a workflow version + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_second_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. + """ + files = {"icon": ("RickRoll.txt", BytesIO(b"Never gonna give you up"), "plain/text")} + response = await client.post( + "/".join( + [ + self.base_path, + str(random_workflow_version.workflow_id), + "versions", + random_workflow_version.git_commit_hash, + "icon", + ] + ), + headers=random_second_user.auth_headers, + files=files, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + @pytest.mark.asyncio async def test_delete_icon( self, client: AsyncClient, random_user: UserWithAuthHeader, - random_workflow: WorkflowOut, random_workflow_version: WorkflowVersion, mock_s3_service: MockS3ServiceResource, db: AsyncSession, @@ -328,8 +440,6 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): HTTP Client to perform the request on. random_user : app.tests.utils.user.UserWithAuthHeader Random user for testing. - random_workflow : app.schemas.workflow.WorkflowOut - Random workflow for testing. random_workflow_version : clowmdb.models.WorkflowVersion Random workflow version for testing. mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource @@ -342,7 +452,7 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): "/".join( [ self.base_path, - str(random_workflow.workflow_id), + str(random_workflow_version.workflow_id), "versions", random_workflow_version.git_commit_hash, "icon", @@ -357,3 +467,42 @@ class TestWorkflowVersionIconRoutes(_TestWorkflowVersionRoutes): ) assert db_version is not None assert db_version.icon_slug is None + + @pytest.mark.asyncio + async def test_delete_icon_as_non_developer( + self, + client: AsyncClient, + random_second_user: UserWithAuthHeader, + random_workflow_version: WorkflowVersion, + mock_s3_service: MockS3ServiceResource, + db: AsyncSession, + ) -> None: + """ + Test for deleting a workflow version icon + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_second_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_workflow_version : clowmdb.models.WorkflowVersion + Random workflow version for testing. + mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource + Mock S3 Service to manipulate objects. + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + """ + response = await client.delete( + "/".join( + [ + self.base_path, + str(random_workflow_version.workflow_id), + "versions", + random_workflow_version.git_commit_hash, + "icon", + ] + ), + headers=random_second_user.auth_headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN -- GitLab