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() 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 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_test.py b/tests/routers/openml/setups_tag_test.py new file mode 100644 index 00000000..44d31f24 --- /dev/null +++ b/tests/routers/openml/setups_tag_test.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_test.py similarity index 54% rename from tests/routers/openml/setups_test.py rename to tests/routers/openml/setups_untag_test.py index ca4e6cd2..5ea8b515 100644 --- a/tests/routers/openml/setups_test.py +++ b/tests/routers/openml/setups_untag_test.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 diff --git a/tests/routers/openml/study_attach_test.py b/tests/routers/openml/study_attach_test.py new file mode 100644 index 00000000..2da1b8f0 --- /dev/null +++ b/tests/routers/openml/study_attach_test.py @@ -0,0 +1,97 @@ +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 = :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}, + ) + + +@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 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()