Skip to content

Commit eb9c646

Browse files
refactor functional tests
1 parent 3f1a7ac commit eb9c646

1 file changed

Lines changed: 127 additions & 118 deletions

File tree

tests/routers/openml/task_list_test.py

Lines changed: 127 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
import httpx
55
import pytest
6+
from sqlalchemy.ext.asyncio import AsyncConnection
67

78
from core.errors import NoResultsError
9+
from routers.dependencies import Pagination
10+
from routers.openml.tasks import TaskStatusFilter, list_tasks
811

912

1013
async def test_list_tasks_default(py_api: httpx.AsyncClient) -> None:
@@ -36,60 +39,138 @@ async def test_list_tasks_get(py_api: httpx.AsyncClient) -> None:
3639
assert isinstance(response.json(), list)
3740

3841

39-
async def test_list_tasks_filter_type(py_api: httpx.AsyncClient) -> None:
42+
@pytest.mark.parametrize(
43+
("limit", "offset", "expected_status", "expected_max_results"),
44+
[
45+
(-10, 0, HTTPStatus.NOT_FOUND, 0), # negative limit clamped to 0 -> No results
46+
(5, -10, HTTPStatus.OK, 5), # negative offset clamped to 0 -> First 5 results
47+
],
48+
ids=["negative_limit", "negative_offset"],
49+
)
50+
async def test_list_tasks_negative_pagination_safely_clamped(
51+
limit: int,
52+
offset: int,
53+
expected_status: int,
54+
expected_max_results: int,
55+
py_api: httpx.AsyncClient,
56+
) -> None:
57+
"""Negative pagination values are safely clamped to 0 instead of causing 500 errors.
58+
59+
A limit clamped to 0 returns a 482 NoResultsError (404 Not Found).
60+
An offset clamped to 0 simply returns the first page of results (200 OK).
61+
62+
Note: This remains an HTTP-level (py_api) test to ensure end-to-end safety is
63+
preserved, especially if validation logic is moved to the Pydantic layer in
64+
the future. Then error will be 422 Unprocessable Entity.
65+
"""
66+
response = await py_api.post(
67+
"/tasks/list",
68+
json={"pagination": {"limit": limit, "offset": offset}},
69+
)
70+
assert response.status_code == expected_status
71+
if expected_status == HTTPStatus.OK:
72+
assert len(response.json()) <= expected_max_results
73+
else:
74+
error = response.json()
75+
assert error["type"] == NoResultsError.uri
76+
77+
78+
@pytest.mark.parametrize(
79+
"pagination_override",
80+
[
81+
{"limit": "abc", "offset": 0}, # Invalid type
82+
{"limit": 5, "offset": "xyz"}, # Invalid type
83+
],
84+
ids=["bad_limit_type", "bad_offset_type"],
85+
)
86+
async def test_list_tasks_invalid_pagination_type(
87+
pagination_override: dict[str, Any], py_api: httpx.AsyncClient
88+
) -> None:
89+
"""Invalid pagination types return 422 Unprocessable Entity."""
90+
response = await py_api.post(
91+
"/tasks/list",
92+
json={"pagination": pagination_override},
93+
)
94+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
95+
96+
97+
@pytest.mark.parametrize(
98+
"value",
99+
["1...2", "abc"],
100+
ids=["triple_dot", "non_numeric"],
101+
)
102+
async def test_list_tasks_invalid_range(value: str, py_api: httpx.AsyncClient) -> None:
103+
"""Invalid number_instances format returns 422 Unprocessable Entity."""
104+
response = await py_api.post("/tasks/list", json={"number_instances": value})
105+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
106+
107+
108+
@pytest.mark.parametrize(
109+
"payload",
110+
[
111+
{"tag": "!@#$% "}, # SystemString64 regex mismatch
112+
{"data_name": "!@#$% "}, # CasualString128 regex mismatch
113+
{"task_id": []}, # min_length=1 violation
114+
{"data_id": []}, # min_length=1 violation
115+
],
116+
ids=["bad_tag_format", "bad_data_name_format", "empty_task_ids", "empty_data_ids"],
117+
)
118+
async def test_list_tasks_invalid_inputs(
119+
payload: dict[str, Any], py_api: httpx.AsyncClient
120+
) -> None:
121+
"""Malformed inputs violating Pydantic/FastAPI constraints return 422."""
122+
response = await py_api.post("/tasks/list", json=payload)
123+
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
124+
125+
126+
# ── Direct call tests: list_tasks ──
127+
128+
129+
async def test_list_tasks_filter_type(expdb_test: AsyncConnection) -> None:
40130
"""Filter by task_type_id returns only tasks of that type."""
41-
response = await py_api.post("/tasks/list", json={"task_type_id": 1})
42-
assert response.status_code == HTTPStatus.OK
43-
tasks = response.json()
131+
tasks = await list_tasks(pagination=Pagination(), task_type_id=1, expdb=expdb_test)
44132
assert len(tasks) > 0
45133
assert all(t["task_type_id"] == 1 for t in tasks)
46134

47135

48-
async def test_list_tasks_filter_tag(py_api: httpx.AsyncClient) -> None:
136+
async def test_list_tasks_filter_tag(expdb_test: AsyncConnection) -> None:
49137
"""Filter by tag returns only tasks with that tag."""
50-
response = await py_api.post("/tasks/list", json={"tag": "OpenML100"})
51-
assert response.status_code == HTTPStatus.OK
52-
tasks = response.json()
138+
tasks = await list_tasks(pagination=Pagination(), tag="OpenML100", expdb=expdb_test)
53139
assert len(tasks) > 0
54140
assert all("OpenML100" in t["tag"] for t in tasks)
55141

56142

57143
@pytest.mark.parametrize("task_id", [1, 59, [1, 2, 3]])
58144
async def test_list_tasks_filter_task_id(
59-
task_id: int | list[int], py_api: httpx.AsyncClient
145+
task_id: int | list[int], expdb_test: AsyncConnection
60146
) -> None:
61147
"""Filter by task_id returns only those tasks."""
62148
ids = [task_id] if isinstance(task_id, int) else task_id
63-
response = await py_api.post("/tasks/list", json={"task_id": ids})
64-
assert response.status_code == HTTPStatus.OK
65-
returned_ids = {t["task_id"] for t in response.json()}
149+
tasks = await list_tasks(pagination=Pagination(), task_id=ids, expdb=expdb_test)
150+
returned_ids = {t["task_id"] for t in tasks}
66151
assert returned_ids == set(ids)
67152

68153

69-
async def test_list_tasks_filter_data_id(py_api: httpx.AsyncClient) -> None:
154+
async def test_list_tasks_filter_data_id(expdb_test: AsyncConnection) -> None:
70155
"""Filter by data_id returns only tasks that use that dataset."""
71156
data_id = 10
72-
response = await py_api.post("/tasks/list", json={"data_id": [data_id]})
73-
assert response.status_code == HTTPStatus.OK
74-
tasks = response.json()
157+
tasks = await list_tasks(pagination=Pagination(), data_id=[data_id], expdb=expdb_test)
75158
assert len(tasks) > 0
76159
assert all(t["did"] == data_id for t in tasks)
77160

78161

79-
async def test_list_tasks_filter_data_name(py_api: httpx.AsyncClient) -> None:
162+
async def test_list_tasks_filter_data_name(expdb_test: AsyncConnection) -> None:
80163
"""Filter by data_name returns only tasks whose dataset matches."""
81-
response = await py_api.post("/tasks/list", json={"data_name": "mfeat-pixel"})
82-
assert response.status_code == HTTPStatus.OK
83-
tasks = response.json()
164+
tasks = await list_tasks(pagination=Pagination(), data_name="mfeat-pixel", expdb=expdb_test)
84165
assert len(tasks) > 0
85166
assert all(t["name"] == "mfeat-pixel" for t in tasks)
86167

87168

88-
async def test_list_tasks_filter_status_deactivated(py_api: httpx.AsyncClient) -> None:
169+
async def test_list_tasks_filter_status_deactivated(expdb_test: AsyncConnection) -> None:
89170
"""Filter by status='deactivated' returns tasks with that status."""
90-
response = await py_api.post("/tasks/list", json={"status": "deactivated"})
91-
assert response.status_code == HTTPStatus.OK
92-
tasks = response.json()
171+
tasks = await list_tasks(
172+
pagination=Pagination(), status=TaskStatusFilter.DEACTIVATED, expdb=expdb_test
173+
)
93174
assert len(tasks) > 0
94175
assert all(t["status"] == "deactivated" for t in tasks)
95176

@@ -98,114 +179,56 @@ async def test_list_tasks_filter_status_deactivated(py_api: httpx.AsyncClient) -
98179
("limit", "offset"),
99180
[(5, 0), (10, 0), (5, 5)],
100181
)
101-
async def test_list_tasks_pagination(limit: int, offset: int, py_api: httpx.AsyncClient) -> None:
182+
async def test_list_tasks_pagination(limit: int, offset: int, expdb_test: AsyncConnection) -> None:
102183
"""Pagination limit and offset are respected."""
103-
response = await py_api.post(
104-
"/tasks/list",
105-
json={"pagination": {"limit": limit, "offset": offset}},
106-
)
107-
assert response.status_code == HTTPStatus.OK
108-
assert len(response.json()) <= limit
184+
tasks = await list_tasks(pagination=Pagination(limit=limit, offset=offset), expdb=expdb_test)
185+
assert len(tasks) <= limit
109186

110187

111-
async def test_list_tasks_pagination_order_stable(py_api: httpx.AsyncClient) -> None:
188+
async def test_list_tasks_pagination_order_stable(expdb_test: AsyncConnection) -> None:
112189
"""Results are ordered by task_id — consecutive pages are in ascending order."""
113-
r1 = await py_api.post("/tasks/list", json={"pagination": {"limit": 5, "offset": 0}})
114-
r2 = await py_api.post("/tasks/list", json={"pagination": {"limit": 5, "offset": 5}})
115-
ids1 = [t["task_id"] for t in r1.json()]
116-
ids2 = [t["task_id"] for t in r2.json()]
190+
tasks1 = await list_tasks(pagination=Pagination(limit=5, offset=0), expdb=expdb_test)
191+
tasks2 = await list_tasks(pagination=Pagination(limit=5, offset=5), expdb=expdb_test)
192+
ids1 = [t["task_id"] for t in tasks1]
193+
ids2 = [t["task_id"] for t in tasks2]
117194
assert ids1 == sorted(ids1)
118195
assert ids2 == sorted(ids2)
119196
if ids1 and ids2:
120197
assert max(ids1) < min(ids2)
121198

122199

123-
async def test_list_tasks_number_instances_range(py_api: httpx.AsyncClient) -> None:
200+
async def test_list_tasks_number_instances_range(expdb_test: AsyncConnection) -> None:
124201
"""number_instances range filter returns tasks whose dataset matches."""
125202
min_instances, max_instances = 100, 1000
126-
response = await py_api.post(
127-
"/tasks/list",
128-
json={"number_instances": f"{min_instances}..{max_instances}"},
203+
tasks = await list_tasks(
204+
pagination=Pagination(),
205+
number_instances=f"{min_instances}..{max_instances}",
206+
expdb=expdb_test,
129207
)
130-
assert response.status_code == HTTPStatus.OK
131-
tasks = response.json()
132208
assert len(tasks) > 0
133209
for task in tasks:
134210
qualities = {q["name"]: q["value"] for q in task["quality"]}
135211
if "NumberOfInstances" in qualities:
136212
assert min_instances <= float(qualities["NumberOfInstances"]) <= max_instances
137213

138214

139-
async def test_list_tasks_inputs_are_basic_subset(py_api: httpx.AsyncClient) -> None:
215+
async def test_list_tasks_inputs_are_basic_subset(expdb_test: AsyncConnection) -> None:
140216
"""Input entries only contain the expected basic input names."""
141217
basic_inputs = {"source_data", "target_feature", "estimation_procedure", "evaluation_measures"}
142-
response = await py_api.post("/tasks/list", json={"pagination": {"limit": 5, "offset": 0}})
143-
assert response.status_code == HTTPStatus.OK
144-
for task in response.json():
218+
tasks = await list_tasks(pagination=Pagination(limit=5, offset=0), expdb=expdb_test)
219+
for task in tasks:
145220
for inp in task["input"]:
146221
assert inp["name"] in basic_inputs
147222

148223

149-
async def test_list_tasks_quality_values_are_strings(py_api: httpx.AsyncClient) -> None:
224+
async def test_list_tasks_quality_values_are_strings(expdb_test: AsyncConnection) -> None:
150225
"""Quality values must be strings (to match PHP API behaviour)."""
151-
response = await py_api.post("/tasks/list", json={"pagination": {"limit": 5, "offset": 0}})
152-
assert response.status_code == HTTPStatus.OK
153-
for task in response.json():
226+
tasks = await list_tasks(pagination=Pagination(limit=5, offset=0), expdb=expdb_test)
227+
for task in tasks:
154228
for quality in task["quality"]:
155229
assert isinstance(quality["value"], str)
156230

157231

158-
@pytest.mark.parametrize(
159-
"pagination_override",
160-
[
161-
{"limit": "abc", "offset": 0}, # Invalid type
162-
{"limit": 5, "offset": "xyz"}, # Invalid type
163-
],
164-
ids=["bad_limit_type", "bad_offset_type"],
165-
)
166-
async def test_list_tasks_invalid_pagination_type(
167-
pagination_override: dict[str, Any], py_api: httpx.AsyncClient
168-
) -> None:
169-
"""Invalid pagination types return 422 Unprocessable Entity."""
170-
response = await py_api.post(
171-
"/tasks/list",
172-
json={"pagination": pagination_override},
173-
)
174-
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
175-
176-
177-
@pytest.mark.parametrize(
178-
("limit", "offset", "expected_status", "expected_max_results"),
179-
[
180-
(-10, 0, HTTPStatus.NOT_FOUND, 0), # negative limit clamped to 0 -> No results
181-
(5, -10, HTTPStatus.OK, 5), # negative offset clamped to 0 -> First 5 results
182-
],
183-
ids=["negative_limit", "negative_offset"],
184-
)
185-
async def test_list_tasks_negative_pagination_safely_clamped(
186-
limit: int,
187-
offset: int,
188-
expected_status: int,
189-
expected_max_results: int,
190-
py_api: httpx.AsyncClient,
191-
) -> None:
192-
"""Negative pagination values are safely clamped to 0 instead of causing 500 errors.
193-
194-
A limit clamped to 0 returns a 482 NoResultsError (404 Not Found).
195-
An offset clamped to 0 simply returns the first page of results (200 OK).
196-
"""
197-
response = await py_api.post(
198-
"/tasks/list",
199-
json={"pagination": {"limit": limit, "offset": offset}},
200-
)
201-
assert response.status_code == expected_status
202-
if expected_status == HTTPStatus.OK:
203-
assert len(response.json()) <= expected_max_results
204-
else:
205-
error = response.json()
206-
assert error["type"] == NoResultsError.uri
207-
208-
209232
@pytest.mark.parametrize(
210233
"payload",
211234
[
@@ -215,21 +238,7 @@ async def test_list_tasks_negative_pagination_safely_clamped(
215238
],
216239
ids=["bad_tag", "bad_task_id", "bad_data_name"],
217240
)
218-
async def test_list_tasks_no_results(payload: dict[str, Any], py_api: httpx.AsyncClient) -> None:
241+
async def test_list_tasks_no_results(payload: dict[str, Any], expdb_test: AsyncConnection) -> None:
219242
"""Filters matching nothing return 404 NoResultsError."""
220-
response = await py_api.post("/tasks/list", json=payload)
221-
assert response.status_code == HTTPStatus.NOT_FOUND
222-
assert response.headers["content-type"] == "application/problem+json"
223-
error = response.json()
224-
assert error["type"] == NoResultsError.uri
225-
226-
227-
@pytest.mark.parametrize(
228-
"value",
229-
["1...2", "abc"],
230-
ids=["triple_dot", "non_numeric"],
231-
)
232-
async def test_list_tasks_invalid_range(value: str, py_api: httpx.AsyncClient) -> None:
233-
"""Invalid number_instances format returns 422 Unprocessable Entity."""
234-
response = await py_api.post("/tasks/list", json={"number_instances": value})
235-
assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
243+
with pytest.raises(NoResultsError):
244+
await list_tasks(pagination=Pagination(), expdb=expdb_test, **payload)

0 commit comments

Comments
 (0)