From 6a91a552b69226c985f19fb13ab3c8e904518d39 Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 14:49:48 +0100 Subject: [PATCH 1/7] Separate out tests by endpoint --- ...ies_test.py => datasets_qualities_test.py} | 162 ----------------- tests/routers/openml/flows_exists_test.py | 81 +++++++++ .../{flows_test.py => flows_get_test.py} | 78 -------- tests/routers/openml/qualities_list_test.py | 167 ++++++++++++++++++ 4 files changed, 248 insertions(+), 240 deletions(-) rename tests/routers/openml/{qualities_test.py => datasets_qualities_test.py} (63%) create mode 100644 tests/routers/openml/flows_exists_test.py rename tests/routers/openml/{flows_test.py => flows_get_test.py} (83%) create mode 100644 tests/routers/openml/qualities_list_test.py diff --git a/tests/routers/openml/qualities_test.py b/tests/routers/openml/datasets_qualities_test.py similarity index 63% rename from tests/routers/openml/qualities_test.py rename to tests/routers/openml/datasets_qualities_test.py index b0b54979..841c320f 100644 --- a/tests/routers/openml/qualities_test.py +++ b/tests/routers/openml/datasets_qualities_test.py @@ -5,168 +5,6 @@ import deepdiff import httpx import pytest -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncConnection - - -async def _remove_quality_from_database(quality_name: str, expdb_test: AsyncConnection) -> None: - await expdb_test.execute( - text( - """ - DELETE FROM data_quality - WHERE `quality`=:deleted_quality - """, - ), - parameters={"deleted_quality": quality_name}, - ) - await expdb_test.execute( - text( - """ - DELETE FROM quality - WHERE `name`=:deleted_quality - """, - ), - parameters={"deleted_quality": quality_name}, - ) - - -async def test_list_qualities_identical( - py_api: httpx.AsyncClient, php_api: httpx.AsyncClient -) -> None: - new, original = await asyncio.gather( - py_api.get("/datasets/qualities/list"), - php_api.get("/data/qualities/list"), - ) - assert original.status_code == new.status_code - assert original.json() == new.json() - # To keep the test idempotent, we cannot test if reaction to database changes is identical - - -@pytest.mark.mut -async def test_list_qualities(py_api: httpx.AsyncClient, expdb_test: AsyncConnection) -> None: - response = await py_api.get("/datasets/qualities/list") - assert response.status_code == HTTPStatus.OK - expected = { - "data_qualities_list": { - "quality": [ - "AutoCorrelation", - "CfsSubsetEval_DecisionStumpAUC", - "CfsSubsetEval_DecisionStumpErrRate", - "CfsSubsetEval_DecisionStumpKappa", - "CfsSubsetEval_NaiveBayesAUC", - "CfsSubsetEval_NaiveBayesErrRate", - "CfsSubsetEval_NaiveBayesKappa", - "CfsSubsetEval_kNN1NAUC", - "CfsSubsetEval_kNN1NErrRate", - "CfsSubsetEval_kNN1NKappa", - "ClassEntropy", - "DecisionStumpAUC", - "DecisionStumpErrRate", - "DecisionStumpKappa", - "Dimensionality", - "EquivalentNumberOfAtts", - "J48.00001.AUC", - "J48.00001.ErrRate", - "J48.00001.Kappa", - "J48.0001.AUC", - "J48.0001.ErrRate", - "J48.0001.Kappa", - "J48.001.AUC", - "J48.001.ErrRate", - "J48.001.Kappa", - "MajorityClassPercentage", - "MajorityClassSize", - "MaxAttributeEntropy", - "MaxKurtosisOfNumericAtts", - "MaxMeansOfNumericAtts", - "MaxMutualInformation", - "MaxNominalAttDistinctValues", - "MaxSkewnessOfNumericAtts", - "MaxStdDevOfNumericAtts", - "MeanAttributeEntropy", - "MeanKurtosisOfNumericAtts", - "MeanMeansOfNumericAtts", - "MeanMutualInformation", - "MeanNoiseToSignalRatio", - "MeanNominalAttDistinctValues", - "MeanSkewnessOfNumericAtts", - "MeanStdDevOfNumericAtts", - "MinAttributeEntropy", - "MinKurtosisOfNumericAtts", - "MinMeansOfNumericAtts", - "MinMutualInformation", - "MinNominalAttDistinctValues", - "MinSkewnessOfNumericAtts", - "MinStdDevOfNumericAtts", - "MinorityClassPercentage", - "MinorityClassSize", - "NaiveBayesAUC", - "NaiveBayesErrRate", - "NaiveBayesKappa", - "NumberOfBinaryFeatures", - "NumberOfClasses", - "NumberOfFeatures", - "NumberOfInstances", - "NumberOfInstancesWithMissingValues", - "NumberOfMissingValues", - "NumberOfNumericFeatures", - "NumberOfSymbolicFeatures", - "PercentageOfBinaryFeatures", - "PercentageOfInstancesWithMissingValues", - "PercentageOfMissingValues", - "PercentageOfNumericFeatures", - "PercentageOfSymbolicFeatures", - "Quartile1AttributeEntropy", - "Quartile1KurtosisOfNumericAtts", - "Quartile1MeansOfNumericAtts", - "Quartile1MutualInformation", - "Quartile1SkewnessOfNumericAtts", - "Quartile1StdDevOfNumericAtts", - "Quartile2AttributeEntropy", - "Quartile2KurtosisOfNumericAtts", - "Quartile2MeansOfNumericAtts", - "Quartile2MutualInformation", - "Quartile2SkewnessOfNumericAtts", - "Quartile2StdDevOfNumericAtts", - "Quartile3AttributeEntropy", - "Quartile3KurtosisOfNumericAtts", - "Quartile3MeansOfNumericAtts", - "Quartile3MutualInformation", - "Quartile3SkewnessOfNumericAtts", - "Quartile3StdDevOfNumericAtts", - "REPTreeDepth1AUC", - "REPTreeDepth1ErrRate", - "REPTreeDepth1Kappa", - "REPTreeDepth2AUC", - "REPTreeDepth2ErrRate", - "REPTreeDepth2Kappa", - "REPTreeDepth3AUC", - "REPTreeDepth3ErrRate", - "REPTreeDepth3Kappa", - "RandomTreeDepth1AUC", - "RandomTreeDepth1ErrRate", - "RandomTreeDepth1Kappa", - "RandomTreeDepth2AUC", - "RandomTreeDepth2ErrRate", - "RandomTreeDepth2Kappa", - "RandomTreeDepth3AUC", - "RandomTreeDepth3ErrRate", - "RandomTreeDepth3Kappa", - "StdvNominalAttDistinctValues", - "kNN1NAUC", - "kNN1NErrRate", - "kNN1NKappa", - ], - }, - } - assert expected == response.json() - - deleted = expected["data_qualities_list"]["quality"].pop() - await _remove_quality_from_database(quality_name=deleted, expdb_test=expdb_test) - - response = await py_api.get("/datasets/qualities/list") - assert response.status_code == HTTPStatus.OK - assert expected == response.json() async def test_get_quality(py_api: httpx.AsyncClient) -> None: diff --git a/tests/routers/openml/flows_exists_test.py b/tests/routers/openml/flows_exists_test.py new file mode 100644 index 00000000..d767b9a3 --- /dev/null +++ b/tests/routers/openml/flows_exists_test.py @@ -0,0 +1,81 @@ +from http import HTTPStatus + +import httpx +import pytest +from pytest_mock import MockerFixture +from sqlalchemy.ext.asyncio import AsyncConnection + +from core.errors import FlowNotFoundError +from routers.openml.flows import flow_exists +from tests.conftest import Flow + + +async def test_flow_exists(flow: Flow, py_api: httpx.AsyncClient) -> None: + response = await py_api.get(f"/flows/exists/{flow.name}/{flow.external_version}") + assert response.status_code == HTTPStatus.OK + assert response.json() == {"flow_id": flow.id} + + +async def test_flow_exists_not_exists(py_api: httpx.AsyncClient) -> None: + name, version = "foo", "bar" + response = await py_api.get(f"/flows/exists/{name}/{version}") + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.headers["content-type"] == "application/problem+json" + error = response.json() + assert error["type"] == FlowNotFoundError.uri + assert name in error["detail"] + assert version in error["detail"] + + +@pytest.mark.parametrize( + ("name", "external_version"), + [ + ("a", "b"), + ("c", "d"), + ], +) +async def test_flow_exists_calls_db_correctly( + name: str, + external_version: str, + expdb_test: AsyncConnection, + mocker: MockerFixture, +) -> None: + mocked_db = mocker.patch( + "database.flows.get_by_name", + new_callable=mocker.AsyncMock, + ) + await flow_exists(name, external_version, expdb_test) + mocked_db.assert_called_once_with( + name=name, + external_version=external_version, + expdb=mocker.ANY, + ) + + +@pytest.mark.parametrize( + "flow_id", + [1, 2], +) +async def test_flow_exists_processes_found( + flow_id: int, + mocker: MockerFixture, + expdb_test: AsyncConnection, +) -> None: + fake_flow = mocker.MagicMock(id=flow_id) + mocker.patch( + "database.flows.get_by_name", + new_callable=mocker.AsyncMock, + return_value=fake_flow, + ) + response = await flow_exists("name", "external_version", expdb_test) + assert response == {"flow_id": fake_flow.id} + + +async def test_flow_exists_handles_flow_not_found( + mocker: MockerFixture, expdb_test: AsyncConnection +) -> None: + mocker.patch("database.flows.get_by_name", return_value=None) + with pytest.raises(FlowNotFoundError) as error: + await flow_exists("foo", "bar", expdb_test) + assert error.value.status_code == HTTPStatus.NOT_FOUND + assert error.value.uri == FlowNotFoundError.uri diff --git a/tests/routers/openml/flows_test.py b/tests/routers/openml/flows_get_test.py similarity index 83% rename from tests/routers/openml/flows_test.py rename to tests/routers/openml/flows_get_test.py index 400ec4c0..e24e7054 100644 --- a/tests/routers/openml/flows_test.py +++ b/tests/routers/openml/flows_get_test.py @@ -2,84 +2,6 @@ import deepdiff.diff import httpx -import pytest -from pytest_mock import MockerFixture -from sqlalchemy.ext.asyncio import AsyncConnection - -from core.errors import FlowNotFoundError -from routers.openml.flows import flow_exists -from tests.conftest import Flow - - -@pytest.mark.parametrize( - ("name", "external_version"), - [ - ("a", "b"), - ("c", "d"), - ], -) -async def test_flow_exists_calls_db_correctly( - name: str, - external_version: str, - expdb_test: AsyncConnection, - mocker: MockerFixture, -) -> None: - mocked_db = mocker.patch( - "database.flows.get_by_name", - new_callable=mocker.AsyncMock, - ) - await flow_exists(name, external_version, expdb_test) - mocked_db.assert_called_once_with( - name=name, - external_version=external_version, - expdb=mocker.ANY, - ) - - -@pytest.mark.parametrize( - "flow_id", - [1, 2], -) -async def test_flow_exists_processes_found( - flow_id: int, - mocker: MockerFixture, - expdb_test: AsyncConnection, -) -> None: - fake_flow = mocker.MagicMock(id=flow_id) - mocker.patch( - "database.flows.get_by_name", - new_callable=mocker.AsyncMock, - return_value=fake_flow, - ) - response = await flow_exists("name", "external_version", expdb_test) - assert response == {"flow_id": fake_flow.id} - - -async def test_flow_exists_handles_flow_not_found( - mocker: MockerFixture, expdb_test: AsyncConnection -) -> None: - mocker.patch("database.flows.get_by_name", return_value=None) - with pytest.raises(FlowNotFoundError) as error: - await flow_exists("foo", "bar", expdb_test) - assert error.value.status_code == HTTPStatus.NOT_FOUND - assert error.value.uri == FlowNotFoundError.uri - - -async def test_flow_exists(flow: Flow, py_api: httpx.AsyncClient) -> None: - response = await py_api.get(f"/flows/exists/{flow.name}/{flow.external_version}") - assert response.status_code == HTTPStatus.OK - assert response.json() == {"flow_id": flow.id} - - -async def test_flow_exists_not_exists(py_api: httpx.AsyncClient) -> None: - name, version = "foo", "bar" - response = await py_api.get(f"/flows/exists/{name}/{version}") - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.headers["content-type"] == "application/problem+json" - error = response.json() - assert error["type"] == FlowNotFoundError.uri - assert name in error["detail"] - assert version in error["detail"] async def test_get_flow_no_subflow(py_api: httpx.AsyncClient) -> None: diff --git a/tests/routers/openml/qualities_list_test.py b/tests/routers/openml/qualities_list_test.py new file mode 100644 index 00000000..6ca21ec0 --- /dev/null +++ b/tests/routers/openml/qualities_list_test.py @@ -0,0 +1,167 @@ +import asyncio +from http import HTTPStatus + +import httpx +import pytest +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection + + +async def _remove_quality_from_database(quality_name: str, expdb_test: AsyncConnection) -> None: + await expdb_test.execute( + text( + """ + DELETE FROM data_quality + WHERE `quality`=:deleted_quality + """, + ), + parameters={"deleted_quality": quality_name}, + ) + await expdb_test.execute( + text( + """ + DELETE FROM quality + WHERE `name`=:deleted_quality + """, + ), + parameters={"deleted_quality": quality_name}, + ) + + +async def test_list_qualities_identical( + py_api: httpx.AsyncClient, php_api: httpx.AsyncClient +) -> None: + new, original = await asyncio.gather( + py_api.get("/datasets/qualities/list"), + php_api.get("/data/qualities/list"), + ) + assert original.status_code == new.status_code + assert original.json() == new.json() + # To keep the test idempotent, we cannot test if reaction to database changes is identical + + +@pytest.mark.mut +async def test_list_qualities(py_api: httpx.AsyncClient, expdb_test: AsyncConnection) -> None: + response = await py_api.get("/datasets/qualities/list") + assert response.status_code == HTTPStatus.OK + expected = { + "data_qualities_list": { + "quality": [ + "AutoCorrelation", + "CfsSubsetEval_DecisionStumpAUC", + "CfsSubsetEval_DecisionStumpErrRate", + "CfsSubsetEval_DecisionStumpKappa", + "CfsSubsetEval_NaiveBayesAUC", + "CfsSubsetEval_NaiveBayesErrRate", + "CfsSubsetEval_NaiveBayesKappa", + "CfsSubsetEval_kNN1NAUC", + "CfsSubsetEval_kNN1NErrRate", + "CfsSubsetEval_kNN1NKappa", + "ClassEntropy", + "DecisionStumpAUC", + "DecisionStumpErrRate", + "DecisionStumpKappa", + "Dimensionality", + "EquivalentNumberOfAtts", + "J48.00001.AUC", + "J48.00001.ErrRate", + "J48.00001.Kappa", + "J48.0001.AUC", + "J48.0001.ErrRate", + "J48.0001.Kappa", + "J48.001.AUC", + "J48.001.ErrRate", + "J48.001.Kappa", + "MajorityClassPercentage", + "MajorityClassSize", + "MaxAttributeEntropy", + "MaxKurtosisOfNumericAtts", + "MaxMeansOfNumericAtts", + "MaxMutualInformation", + "MaxNominalAttDistinctValues", + "MaxSkewnessOfNumericAtts", + "MaxStdDevOfNumericAtts", + "MeanAttributeEntropy", + "MeanKurtosisOfNumericAtts", + "MeanMeansOfNumericAtts", + "MeanMutualInformation", + "MeanNoiseToSignalRatio", + "MeanNominalAttDistinctValues", + "MeanSkewnessOfNumericAtts", + "MeanStdDevOfNumericAtts", + "MinAttributeEntropy", + "MinKurtosisOfNumericAtts", + "MinMeansOfNumericAtts", + "MinMutualInformation", + "MinNominalAttDistinctValues", + "MinSkewnessOfNumericAtts", + "MinStdDevOfNumericAtts", + "MinorityClassPercentage", + "MinorityClassSize", + "NaiveBayesAUC", + "NaiveBayesErrRate", + "NaiveBayesKappa", + "NumberOfBinaryFeatures", + "NumberOfClasses", + "NumberOfFeatures", + "NumberOfInstances", + "NumberOfInstancesWithMissingValues", + "NumberOfMissingValues", + "NumberOfNumericFeatures", + "NumberOfSymbolicFeatures", + "PercentageOfBinaryFeatures", + "PercentageOfInstancesWithMissingValues", + "PercentageOfMissingValues", + "PercentageOfNumericFeatures", + "PercentageOfSymbolicFeatures", + "Quartile1AttributeEntropy", + "Quartile1KurtosisOfNumericAtts", + "Quartile1MeansOfNumericAtts", + "Quartile1MutualInformation", + "Quartile1SkewnessOfNumericAtts", + "Quartile1StdDevOfNumericAtts", + "Quartile2AttributeEntropy", + "Quartile2KurtosisOfNumericAtts", + "Quartile2MeansOfNumericAtts", + "Quartile2MutualInformation", + "Quartile2SkewnessOfNumericAtts", + "Quartile2StdDevOfNumericAtts", + "Quartile3AttributeEntropy", + "Quartile3KurtosisOfNumericAtts", + "Quartile3MeansOfNumericAtts", + "Quartile3MutualInformation", + "Quartile3SkewnessOfNumericAtts", + "Quartile3StdDevOfNumericAtts", + "REPTreeDepth1AUC", + "REPTreeDepth1ErrRate", + "REPTreeDepth1Kappa", + "REPTreeDepth2AUC", + "REPTreeDepth2ErrRate", + "REPTreeDepth2Kappa", + "REPTreeDepth3AUC", + "REPTreeDepth3ErrRate", + "REPTreeDepth3Kappa", + "RandomTreeDepth1AUC", + "RandomTreeDepth1ErrRate", + "RandomTreeDepth1Kappa", + "RandomTreeDepth2AUC", + "RandomTreeDepth2ErrRate", + "RandomTreeDepth2Kappa", + "RandomTreeDepth3AUC", + "RandomTreeDepth3ErrRate", + "RandomTreeDepth3Kappa", + "StdvNominalAttDistinctValues", + "kNN1NAUC", + "kNN1NErrRate", + "kNN1NKappa", + ], + }, + } + assert expected == response.json() + + deleted = expected["data_qualities_list"]["quality"].pop() + await _remove_quality_from_database(quality_name=deleted, expdb_test=expdb_test) + + response = await py_api.get("/datasets/qualities/list") + assert response.status_code == HTTPStatus.OK + assert expected == response.json() From ce0f14307fa7486c63c4c3bb3fece1a4d0faa02b Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 14:50:41 +0100 Subject: [PATCH 2/7] Rename test file to match endpoint it tests --- tests/routers/openml/{runs_test.py => runs_trace_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/routers/openml/{runs_test.py => runs_trace_test.py} (100%) diff --git a/tests/routers/openml/runs_test.py b/tests/routers/openml/runs_trace_test.py similarity index 100% rename from tests/routers/openml/runs_test.py rename to tests/routers/openml/runs_trace_test.py From a86a6191b85cbb3788d1e5c8b07c441de21dfb5d Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 14:54:42 +0100 Subject: [PATCH 3/7] Separate out tests by endpoint --- tests/routers/openml/setups_get_test.py | 18 ++++++ tests/routers/openml/setups_tag.py | 56 +++++++++++++++++ .../{setups_test.py => setups_untag.py} | 61 ------------------- 3 files changed, 74 insertions(+), 61 deletions(-) create mode 100644 tests/routers/openml/setups_get_test.py create mode 100644 tests/routers/openml/setups_tag.py rename tests/routers/openml/{setups_test.py => setups_untag.py} (54%) diff --git a/tests/routers/openml/setups_get_test.py b/tests/routers/openml/setups_get_test.py new file mode 100644 index 00000000..90094ac6 --- /dev/null +++ b/tests/routers/openml/setups_get_test.py @@ -0,0 +1,18 @@ +import re +from http import HTTPStatus + +import httpx + + +async def test_get_setup_unknown(py_api: httpx.AsyncClient) -> None: + response = await py_api.get("/setup/999999") + assert response.status_code == HTTPStatus.NOT_FOUND + assert re.match(r"Setup \d+ not found.", response.json()["detail"]) + + +async def test_get_setup_success(py_api: httpx.AsyncClient) -> None: + response = await py_api.get("/setup/1") + assert response.status_code == HTTPStatus.OK + data = response.json()["setup_parameters"] + assert data["setup_id"] == 1 + assert "parameter" in data diff --git a/tests/routers/openml/setups_tag.py b/tests/routers/openml/setups_tag.py new file mode 100644 index 00000000..44d31f24 --- /dev/null +++ b/tests/routers/openml/setups_tag.py @@ -0,0 +1,56 @@ +import re +from http import HTTPStatus + +import httpx +import pytest +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection + +from tests.users import ApiKey + + +async def test_setup_tag_missing_auth(py_api: httpx.AsyncClient) -> None: + response = await py_api.post("/setup/tag", json={"setup_id": 1, "tag": "test_tag"}) + assert response.status_code == HTTPStatus.UNAUTHORIZED + assert response.json()["code"] == "103" + assert response.json()["detail"] == "Authentication failed" + + +async def test_setup_tag_unknown_setup(py_api: httpx.AsyncClient) -> None: + response = await py_api.post( + f"/setup/tag?api_key={ApiKey.SOME_USER}", + json={"setup_id": 999999, "tag": "test_tag"}, + ) + assert response.status_code == HTTPStatus.NOT_FOUND + assert re.match(r"Setup \d+ not found.", response.json()["detail"]) + + +@pytest.mark.mut +async def test_setup_tag_already_exists( + py_api: httpx.AsyncClient, expdb_test: AsyncConnection +) -> None: + await expdb_test.execute( + text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, 'existing_tag_123', 2);") + ) + response = await py_api.post( + f"/setup/tag?api_key={ApiKey.SOME_USER}", + json={"setup_id": 1, "tag": "existing_tag_123"}, + ) + assert response.status_code == HTTPStatus.CONFLICT + assert response.json()["detail"] == "Setup 1 already has tag 'existing_tag_123'." + + +@pytest.mark.mut +async def test_setup_tag_success(py_api: httpx.AsyncClient, expdb_test: AsyncConnection) -> None: + response = await py_api.post( + f"/setup/tag?api_key={ApiKey.SOME_USER}", + json={"setup_id": 1, "tag": "my_new_success_tag"}, + ) + + assert response.status_code == HTTPStatus.OK + assert "my_new_success_tag" in response.json()["setup_tag"]["tag"] + + rows = await expdb_test.execute( + text("SELECT * FROM setup_tag WHERE id = 1 AND tag = 'my_new_success_tag'") + ) + assert len(rows.all()) == 1 diff --git a/tests/routers/openml/setups_test.py b/tests/routers/openml/setups_untag.py similarity index 54% rename from tests/routers/openml/setups_test.py rename to tests/routers/openml/setups_untag.py index ca4e6cd2..5ea8b515 100644 --- a/tests/routers/openml/setups_test.py +++ b/tests/routers/openml/setups_untag.py @@ -83,64 +83,3 @@ async def test_setup_untag_success( text("SELECT * FROM setup_tag WHERE id = 1 AND tag = 'test_success_tag'") ) assert len(rows.all()) == 0 - - -async def test_setup_tag_missing_auth(py_api: httpx.AsyncClient) -> None: - response = await py_api.post("/setup/tag", json={"setup_id": 1, "tag": "test_tag"}) - assert response.status_code == HTTPStatus.UNAUTHORIZED - assert response.json()["code"] == "103" - assert response.json()["detail"] == "Authentication failed" - - -async def test_setup_tag_unknown_setup(py_api: httpx.AsyncClient) -> None: - response = await py_api.post( - f"/setup/tag?api_key={ApiKey.SOME_USER}", - json={"setup_id": 999999, "tag": "test_tag"}, - ) - assert response.status_code == HTTPStatus.NOT_FOUND - assert re.match(r"Setup \d+ not found.", response.json()["detail"]) - - -@pytest.mark.mut -async def test_setup_tag_already_exists( - py_api: httpx.AsyncClient, expdb_test: AsyncConnection -) -> None: - await expdb_test.execute( - text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, 'existing_tag_123', 2);") - ) - response = await py_api.post( - f"/setup/tag?api_key={ApiKey.SOME_USER}", - json={"setup_id": 1, "tag": "existing_tag_123"}, - ) - assert response.status_code == HTTPStatus.CONFLICT - assert response.json()["detail"] == "Setup 1 already has tag 'existing_tag_123'." - - -@pytest.mark.mut -async def test_setup_tag_success(py_api: httpx.AsyncClient, expdb_test: AsyncConnection) -> None: - response = await py_api.post( - f"/setup/tag?api_key={ApiKey.SOME_USER}", - json={"setup_id": 1, "tag": "my_new_success_tag"}, - ) - - assert response.status_code == HTTPStatus.OK - assert "my_new_success_tag" in response.json()["setup_tag"]["tag"] - - rows = await expdb_test.execute( - text("SELECT * FROM setup_tag WHERE id = 1 AND tag = 'my_new_success_tag'") - ) - assert len(rows.all()) == 1 - - -async def test_get_setup_unknown(py_api: httpx.AsyncClient) -> None: - response = await py_api.get("/setup/999999") - assert response.status_code == HTTPStatus.NOT_FOUND - assert re.match(r"Setup \d+ not found.", response.json()["detail"]) - - -async def test_get_setup_success(py_api: httpx.AsyncClient) -> None: - response = await py_api.get("/setup/1") - assert response.status_code == HTTPStatus.OK - data = response.json()["setup_parameters"] - assert data["setup_id"] == 1 - assert "parameter" in data From 234ec7066518a35da74381351d56dc4429c105cc Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 14:58:37 +0100 Subject: [PATCH 4/7] Separate tests out by endpoint --- tests/routers/openml/study_attach_test.py | 94 +++++++++++++++++++ .../{study_test.py => study_get_test.py} | 0 tests/routers/openml/study_post_test.py | 50 ++++++++++ 3 files changed, 144 insertions(+) create mode 100644 tests/routers/openml/study_attach_test.py rename tests/routers/openml/{study_test.py => study_get_test.py} (100%) create mode 100644 tests/routers/openml/study_post_test.py diff --git a/tests/routers/openml/study_attach_test.py b/tests/routers/openml/study_attach_test.py new file mode 100644 index 00000000..c382f071 --- /dev/null +++ b/tests/routers/openml/study_attach_test.py @@ -0,0 +1,94 @@ +from http import HTTPStatus + +import httpx +import pytest +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection + +from core.errors import StudyConflictError +from schemas.study import StudyType +from tests.users import ApiKey + + +async def _attach_tasks_to_study( + study_id: int, + task_ids: list[int], + api_key: str, + py_api: httpx.AsyncClient, + expdb_test: AsyncConnection, +) -> httpx.Response: + # Adding requires the study to be in preparation, + # but the current snapshot has no in-preparation studies. + await expdb_test.execute(text("UPDATE study SET status = 'in_preparation' WHERE id = 1")) + return await py_api.post( + f"/studies/attach?api_key={api_key}", + json={"study_id": study_id, "entity_ids": task_ids}, + ) + + +@pytest.mark.mut +async def test_attach_task_to_study(py_api: httpx.AsyncClient, expdb_test: AsyncConnection) -> None: + response = await _attach_tasks_to_study( + study_id=1, + task_ids=[2, 3, 4], + api_key=ApiKey.ADMIN, + py_api=py_api, + expdb_test=expdb_test, + ) + assert response.status_code == HTTPStatus.OK, response.content + assert response.json() == {"study_id": 1, "main_entity_type": StudyType.TASK} + + +@pytest.mark.mut +async def test_attach_task_to_study_needs_owner( + py_api: httpx.AsyncClient, expdb_test: AsyncConnection +) -> None: + await expdb_test.execute(text("UPDATE study SET status = 'in_preparation' WHERE id = 1")) + response = await _attach_tasks_to_study( + study_id=1, + task_ids=[2, 3, 4], + api_key=ApiKey.OWNER_USER, + py_api=py_api, + expdb_test=expdb_test, + ) + assert response.status_code == HTTPStatus.FORBIDDEN, response.content + + +@pytest.mark.mut +async def test_attach_task_to_study_already_linked_raises( + py_api: httpx.AsyncClient, + expdb_test: AsyncConnection, +) -> None: + await expdb_test.execute(text("UPDATE study SET status = 'in_preparation' WHERE id = 1")) + response = await _attach_tasks_to_study( + study_id=1, + task_ids=[1, 3, 4], + api_key=ApiKey.ADMIN, + py_api=py_api, + expdb_test=expdb_test, + ) + assert response.status_code == HTTPStatus.CONFLICT, response.content + assert response.headers["content-type"] == "application/problem+json" + error = response.json() + assert error["type"] == StudyConflictError.uri + assert error["detail"] == "Task 1 is already attached to study 1." + + +@pytest.mark.mut +async def test_attach_task_to_study_but_task_not_exist_raises( + py_api: httpx.AsyncClient, + expdb_test: AsyncConnection, +) -> None: + await expdb_test.execute(text("UPDATE study SET status = 'in_preparation' WHERE id = 1")) + response = await _attach_tasks_to_study( + study_id=1, + task_ids=[80123, 78914], + api_key=ApiKey.ADMIN, + py_api=py_api, + expdb_test=expdb_test, + ) + assert response.status_code == HTTPStatus.CONFLICT + assert response.headers["content-type"] == "application/problem+json" + error = response.json() + assert error["type"] == StudyConflictError.uri + assert error["detail"] == "One or more of the tasks do not exist." diff --git a/tests/routers/openml/study_test.py b/tests/routers/openml/study_get_test.py similarity index 100% rename from tests/routers/openml/study_test.py rename to tests/routers/openml/study_get_test.py diff --git a/tests/routers/openml/study_post_test.py b/tests/routers/openml/study_post_test.py new file mode 100644 index 00000000..df0e5813 --- /dev/null +++ b/tests/routers/openml/study_post_test.py @@ -0,0 +1,50 @@ +from datetime import UTC, datetime +from http import HTTPStatus + +import httpx +import pytest + +from tests.users import ApiKey + + +@pytest.mark.mut +async def test_create_task_study(py_api: httpx.AsyncClient) -> None: + response = await py_api.post( + f"/studies?api_key={ApiKey.SOME_USER}", + json={ + "name": "Test Study", + "alias": "test-study", + "main_entity_type": "task", + "description": "A test study", + "tasks": [1, 2, 3], + "runs": [], + }, + ) + assert response.status_code == HTTPStatus.OK + new = response.json() + assert "study_id" in new + study_id = new["study_id"] + assert isinstance(study_id, int) + + study = await py_api.get(f"/studies/{study_id}") + assert study.status_code == HTTPStatus.OK + expected = { + "id": study_id, + "alias": "test-study", + "main_entity_type": "task", + "name": "Test Study", + "description": "A test study", + "visibility": "public", + "status": "in_preparation", + "creator": 2, + "data_ids": [1, 1, 1], + "task_ids": [1, 2, 3], + "run_ids": [], + "flow_ids": [], + "setup_ids": [], + } + new_study = study.json() + + creation_date = datetime.fromisoformat(new_study.pop("creation_date")) + assert creation_date.date() == datetime.now(UTC).date() + assert new_study == expected From 559c05c36489319d5a87cc41aa6c1f182d573630 Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 14:59:05 +0100 Subject: [PATCH 5/7] Add the _test prefix to make sure pytest collects the tests --- tests/routers/openml/{setups_tag.py => setups_tag_test.py} | 0 tests/routers/openml/{setups_untag.py => setups_untag_test.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/routers/openml/{setups_tag.py => setups_tag_test.py} (100%) rename tests/routers/openml/{setups_untag.py => setups_untag_test.py} (100%) diff --git a/tests/routers/openml/setups_tag.py b/tests/routers/openml/setups_tag_test.py similarity index 100% rename from tests/routers/openml/setups_tag.py rename to tests/routers/openml/setups_tag_test.py diff --git a/tests/routers/openml/setups_untag.py b/tests/routers/openml/setups_untag_test.py similarity index 100% rename from tests/routers/openml/setups_untag.py rename to tests/routers/openml/setups_untag_test.py From 985aa26e1aa0af05ce5ae3aadc651eab9ac293b7 Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 15:01:07 +0100 Subject: [PATCH 6/7] Separate out tests by endpoint --- .../openml/{task_test.py => task_get_test.py} | 0 .../{task_type_test.py => task_type_get_test.py} | 9 --------- tests/routers/openml/task_type_list_test.py | 12 ++++++++++++ 3 files changed, 12 insertions(+), 9 deletions(-) rename tests/routers/openml/{task_test.py => task_get_test.py} (100%) rename tests/routers/openml/{task_type_test.py => task_type_get_test.py} (81%) create mode 100644 tests/routers/openml/task_type_list_test.py diff --git a/tests/routers/openml/task_test.py b/tests/routers/openml/task_get_test.py similarity index 100% rename from tests/routers/openml/task_test.py rename to tests/routers/openml/task_get_test.py diff --git a/tests/routers/openml/task_type_test.py b/tests/routers/openml/task_type_get_test.py similarity index 81% rename from tests/routers/openml/task_type_test.py rename to tests/routers/openml/task_type_get_test.py index 9d515965..ef8e5549 100644 --- a/tests/routers/openml/task_type_test.py +++ b/tests/routers/openml/task_type_get_test.py @@ -8,15 +8,6 @@ from core.errors import TaskTypeNotFoundError -async def test_list_task_type(py_api: httpx.AsyncClient, php_api: httpx.AsyncClient) -> None: - response, original = await asyncio.gather( - py_api.get("/tasktype/list"), - php_api.get("/tasktype/list"), - ) - assert response.status_code == original.status_code - assert response.json() == original.json() - - @pytest.mark.parametrize( "ttype_id", list(range(1, 12)), diff --git a/tests/routers/openml/task_type_list_test.py b/tests/routers/openml/task_type_list_test.py new file mode 100644 index 00000000..d562838b --- /dev/null +++ b/tests/routers/openml/task_type_list_test.py @@ -0,0 +1,12 @@ +import asyncio + +import httpx + + +async def test_list_task_type(py_api: httpx.AsyncClient, php_api: httpx.AsyncClient) -> None: + response, original = await asyncio.gather( + py_api.get("/tasktype/list"), + php_api.get("/tasktype/list"), + ) + assert response.status_code == original.status_code + assert response.json() == original.json() From a548b20c8618ac0a3594a15e8139b687dec76012 Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Fri, 27 Mar 2026 15:12:25 +0100 Subject: [PATCH 7/7] Use the provided study_id instead of a hardcoded value --- tests/routers/openml/study_attach_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/routers/openml/study_attach_test.py b/tests/routers/openml/study_attach_test.py index c382f071..2da1b8f0 100644 --- a/tests/routers/openml/study_attach_test.py +++ b/tests/routers/openml/study_attach_test.py @@ -19,7 +19,10 @@ async def _attach_tasks_to_study( ) -> httpx.Response: # Adding requires the study to be in preparation, # but the current snapshot has no in-preparation studies. - await expdb_test.execute(text("UPDATE study SET status = 'in_preparation' WHERE id = 1")) + await expdb_test.execute( + text("UPDATE study SET status = 'in_preparation' WHERE id = :study_id"), + parameters={"study_id": study_id}, + ) return await py_api.post( f"/studies/attach?api_key={api_key}", json={"study_id": study_id, "entity_ids": task_ids},