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},