diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/pools.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/pools.py index 8cdd25072270a..729b82f3bf89f 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/pools.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/pools.py @@ -49,7 +49,7 @@ @pools_router.delete( - "/{pool_name}", + "/{pool_name:path}", status_code=status.HTTP_204_NO_CONTENT, responses=create_openapi_http_exception_doc( [ @@ -74,7 +74,7 @@ def delete_pool( @pools_router.get( - "/{pool_name}", + "/{pool_name:path}", responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), dependencies=[Depends(requires_access_pool(method="GET"))], ) @@ -124,7 +124,7 @@ def get_pools( @pools_router.patch( - "/{pool_name}", + "/{pool_name:path}", responses=create_openapi_http_exception_doc( [ status.HTTP_400_BAD_REQUEST, diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py index 68c2375913657..342b7edfde088 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_pools.py @@ -37,11 +37,23 @@ POOL2_DESCRIPTION = "Some Description" +POOL3_NAME = "pool3/with_slashes" +POOL3_SLOT = 5 +POOL3_INCLUDE_DEFERRED = False +POOL3_DESCRIPTION = "Some Description" + + @provide_session def _create_pools(session) -> None: pool1 = Pool(pool=POOL1_NAME, slots=POOL1_SLOT, include_deferred=POOL1_INCLUDE_DEFERRED) pool2 = Pool(pool=POOL2_NAME, slots=POOL2_SLOT, include_deferred=POOL2_INCLUDE_DEFERRED) - session.add_all([pool1, pool2]) + pool3 = Pool( + pool=POOL3_NAME, + slots=POOL3_SLOT, + include_deferred=POOL3_INCLUDE_DEFERRED, + description=POOL3_DESCRIPTION, + ) + session.add_all([pool1, pool2, pool3]) class TestPoolsEndpoint: @@ -60,11 +72,11 @@ class TestDeletePool(TestPoolsEndpoint): def test_delete_should_respond_204(self, test_client, session): self.create_pools() pools = session.query(Pool).all() - assert len(pools) == 3 + assert len(pools) == 4 response = test_client.delete(f"/pools/{POOL1_NAME}") assert response.status_code == 204 pools = session.query(Pool).all() - assert len(pools) == 2 + assert len(pools) == 3 check_last_log(session, dag_id=None, event="delete_pool", logical_date=None) def test_delete_should_respond_401(self, unauthenticated_test_client): @@ -87,6 +99,17 @@ def test_delete_should_respond_404(self, test_client): body = response.json() assert f"The Pool with name: `{POOL1_NAME}` was not found" == body["detail"] + def test_delete_pool3_should_respond_204(self, test_client, session): + """Test deleting POOL3 with forward slash in name""" + self.create_pools() + pools = session.query(Pool).all() + assert len(pools) == 4 + response = test_client.delete(f"/pools/{POOL3_NAME}") + assert response.status_code == 204 + pools = session.query(Pool).all() + assert len(pools) == 3 + check_last_log(session, dag_id=None, event="delete_pool", logical_date=None) + class TestGetPool(TestPoolsEndpoint): def test_get_should_respond_200(self, test_client, session): @@ -120,24 +143,42 @@ def test_get_should_respond_404(self, test_client): body = response.json() assert f"The Pool with name: `{POOL1_NAME}` was not found" == body["detail"] + def test_get_pool3_should_respond_200(self, test_client, session): + """Test getting POOL3 with forward slash in name""" + self.create_pools() + response = test_client.get(f"/pools/{POOL3_NAME}") + assert response.status_code == 200 + assert response.json() == { + "deferred_slots": 0, + "description": "Some Description", + "include_deferred": False, + "name": "pool3/with_slashes", + "occupied_slots": 0, + "open_slots": 5, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 5, + } + class TestGetPools(TestPoolsEndpoint): @pytest.mark.parametrize( "query_params, expected_total_entries, expected_ids", [ # Filters - ({}, 3, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME]), - ({"limit": 1}, 3, [Pool.DEFAULT_POOL_NAME]), - ({"limit": 1, "offset": 1}, 3, [POOL1_NAME]), + ({}, 4, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME, POOL3_NAME]), + ({"limit": 1}, 4, [Pool.DEFAULT_POOL_NAME]), + ({"limit": 1, "offset": 1}, 4, [POOL1_NAME]), # Sort - ({"order_by": "-id"}, 3, [POOL2_NAME, POOL1_NAME, Pool.DEFAULT_POOL_NAME]), - ({"order_by": "id"}, 3, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME]), - ({"order_by": "name"}, 3, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME]), + ({"order_by": "-id"}, 4, [POOL3_NAME, POOL2_NAME, POOL1_NAME, Pool.DEFAULT_POOL_NAME]), + ({"order_by": "id"}, 4, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME, POOL3_NAME]), + ({"order_by": "name"}, 4, [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME, POOL3_NAME]), # Search ( {"pool_name_pattern": "~"}, - 3, - [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME], + 4, + [Pool.DEFAULT_POOL_NAME, POOL1_NAME, POOL2_NAME, POOL3_NAME], ), ({"pool_name_pattern": "default"}, 1, [Pool.DEFAULT_POOL_NAME]), ], @@ -314,6 +355,32 @@ def test_should_respond_403(self, unauthorized_test_client): response = unauthorized_test_client.patch(f"/pools/{POOL1_NAME}", params={}, json={}) assert response.status_code == 403 + def test_patch_pool3_should_respond_200(self, test_client, session): + """Test patching POOL3 with forward slash in name""" + self.create_pools() + body = { + "slots": 10, + "description": "Updated Description", + "name": POOL3_NAME, + "include_deferred": True, + } + response = test_client.patch(f"/pools/{POOL3_NAME}", json=body) + assert response.status_code == 200 + expected_response = { + "deferred_slots": 0, + "description": "Updated Description", + "include_deferred": True, + "name": "pool3/with_slashes", + "occupied_slots": 0, + "open_slots": 10, + "queued_slots": 0, + "running_slots": 0, + "scheduled_slots": 0, + "slots": 10, + } + assert response.json() == expected_response + check_last_log(session, dag_id=None, event="patch_pool", logical_date=None) + class TestPostPool(TestPoolsEndpoint): @pytest.mark.parametrize(