Skip to content

Commit 12ee35a

Browse files
authored
Merge pull request #914 from RS-PYTHON/fix/rspy448-staging-jobs-wrong-id-handling
fix-rspy448-staging-jobs-wrong-id-handling
2 parents 970108e + 86b6091 commit 12ee35a

File tree

2 files changed

+110
-55
lines changed

2 files changed

+110
-55
lines changed

services/staging/rs_server_staging/main.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from dask.distributed import LocalCluster
2525
from fastapi import APIRouter, FastAPI, HTTPException, Path
2626
from pygeoapi.api import API
27+
from pygeoapi.process.base import JobNotFoundError
2728
from pygeoapi.process.manager.postgresql import PostgreSQLManager
2829
from pygeoapi.provider.postgresql import get_engine
2930
from rs_server_common.authentication.authentication_to_external import (
@@ -284,10 +285,11 @@ async def execute_process(req: Request, resource: str, data: ProcessMetadataMode
284285
@router.get("/jobs/{job_id}")
285286
async def get_job_status_endpoint(job_id: str = Path(..., title="The ID of the job")):
286287
"""Used to get status of processing job."""
287-
job = app.extra["process_manager"].get_job(job_id)
288-
if job:
289-
return job
290-
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found")
288+
try:
289+
return app.extra["process_manager"].get_job(job_id)
290+
except JobNotFoundError as error:
291+
# Handle case when job_id is not found
292+
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found") from error
291293

292294

293295
@router.get("/jobs")
@@ -303,21 +305,24 @@ async def get_jobs_endpoint():
303305
@router.delete("/jobs/{job_id}")
304306
async def delete_job_endpoint(job_id: str = Path(..., title="The ID of the job to delete")):
305307
"""Deletes a specific job from the database."""
306-
success = app.extra["process_manager"].delete_job(job_id)
307-
if success:
308+
try:
309+
app.extra["process_manager"].delete_job(job_id)
308310
return {"message": f"Job {job_id} deleted successfully"}
309-
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found")
311+
except JobNotFoundError as error:
312+
# Handle case when job_id is not found
313+
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found") from error
310314

311315

312316
@router.get("/jobs/{job_id}/results")
313317
async def get_specific_job_result_endpoint(job_id: str = Path(..., title="The ID of the job")):
314318
"""Get result from a specific job."""
315-
# Query the database to find the job by job_id
316-
job = app.extra["process_manager"].get_job(job_id)
317-
if job:
319+
try:
320+
# Query the database to find the job by job_id
321+
job = app.extra["process_manager"].get_job(job_id)
318322
return JSONResponse(status_code=HTTP_200_OK, content=job["status"])
319-
320-
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found")
323+
except JobNotFoundError as error:
324+
# Handle case when job_id is not found
325+
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Job with ID {job_id} not found") from error
321326

322327

323328
# Configure OpenTelemetry

services/staging/tests/test_staging.py

Lines changed: 93 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import pytest
2020
from fastapi import FastAPI
21+
from pygeoapi.process.base import JobNotFoundError
2122
from rs_server_staging.main import (
2223
app_lifespan,
2324
get_config_contents,
@@ -64,14 +65,6 @@
6465
"created": str(datetime(2024, 1, 4, 12, 0, 0)),
6566
"updated": str(datetime(2024, 1, 4, 13, 0, 0)),
6667
},
67-
{
68-
"identifier": "non_existing",
69-
"status": "successful",
70-
"progress": 100.0,
71-
"message": "Test detail",
72-
"created": "unknown",
73-
"updated": "unknown",
74-
},
7568
]
7669

7770

@@ -227,15 +220,24 @@ async def test_get_jobs_endpoint(mocker, set_db_env_var, staging_client): # pyl
227220

228221
@pytest.mark.asyncio
229222
@pytest.mark.parametrize(
230-
"expected_job",
231-
expected_jobs_test,
223+
"expected_job, expected_status, expected_response",
224+
[
225+
(
226+
{"identifier": "non_existing_id"},
227+
HTTP_404_NOT_FOUND,
228+
{"message": "Job with ID non_existing_id not found"},
229+
),
230+
*[(job, HTTP_200_OK, job) for job in expected_jobs_test],
231+
],
232232
)
233233
async def test_get_job(
234234
mocker,
235235
set_db_env_var, # pylint: disable=unused-argument
236236
staging_client,
237237
mock_jobs,
238-
expected_job, # pylint: disable=unused-argument
238+
expected_job,
239+
expected_status,
240+
expected_response,
239241
):
240242
"""
241243
Test the GET /jobs/{job_id} endpoint for retrieving job details.
@@ -247,8 +249,11 @@ async def test_get_job(
247249
Args:
248250
mocker: A mocker object used to create mocks and patches for testing.
249251
staging_client: A test client for making requests to the FastAPI application.
252+
mock_jobs: Fixture used to mock output of tiny db jobs
250253
expected_job (dict): The expected job dictionary containing job_id,
251254
status, progress, and message for the job to be retrieved.
255+
expected_status: response HTTP status code
256+
expected_response: response body (JSON object)
252257
253258
Assertions:
254259
- Asserts that the response status code is 200 and the returned job
@@ -257,34 +262,47 @@ async def test_get_job(
257262
"""
258263
# Mock app.extra to ensure 'db_table' exists
259264
mock_db_table = mocker.MagicMock()
260-
try:
261-
job_index = next(i for i, job in enumerate(mock_jobs) if job["identifier"] == expected_job["identifier"])
262-
mock_db_table.get_job.return_value = mock_jobs[job_index]
263-
except StopIteration:
264-
mock_db_table.get_job.return_value = []
265+
266+
# Simulate JobNotFoundError for non-existing jobs (HTTP 404)
267+
if expected_status == HTTP_404_NOT_FOUND:
268+
mock_db_table.get_job.side_effect = JobNotFoundError
269+
# Return an existing job normally (HTTP 200)
270+
else:
271+
mock_db_table.get_job.return_value = next(
272+
job for job in mock_jobs if job["identifier"] == expected_job["identifier"]
273+
)
265274

266275
# Patch app.extra with the mock db_table
267276
mocker.patch.object(staging_client.app, "extra", {"process_manager": mock_db_table})
268277

269278
# Call the API
270279
response = staging_client.get(f"/jobs/{expected_job['identifier']}")
271-
# assert response is OK and job info match, or not found for last case
272-
assert (
273-
response.status_code == HTTP_200_OK and response.json() == expected_job
274-
) or response.status_code == HTTP_404_NOT_FOUND
280+
281+
# Assert response status code and content
282+
assert response.status_code == expected_status
283+
assert response.json() == expected_response
275284

276285

277286
@pytest.mark.asyncio
278287
@pytest.mark.parametrize(
279-
"expected_job",
280-
expected_jobs_test,
288+
"expected_job, expected_status, expected_response",
289+
[
290+
(
291+
{"identifier": "non_existing_id"},
292+
HTTP_404_NOT_FOUND,
293+
{"message": "Job with ID non_existing_id not found"},
294+
),
295+
*[(job, HTTP_200_OK, job["status"]) for job in expected_jobs_test],
296+
],
281297
)
282298
async def test_get_job_result(
283299
mocker,
284300
set_db_env_var, # pylint: disable=unused-argument
285301
staging_client,
286302
mock_jobs,
287-
expected_job, # pylint: disable=unused-argument
303+
expected_job,
304+
expected_status,
305+
expected_response,
288306
):
289307
"""
290308
Test the GET /jobs/{job_id}/results endpoint for retrieving job results.
@@ -296,8 +314,11 @@ async def test_get_job_result(
296314
Args:
297315
mocker: A mocker object used to create mocks and patches for testing.
298316
staging_client: A test client for making requests to the FastAPI application.
317+
mock_jobs: Fixture used to mock output of tiny db jobs
299318
expected_job (dict): The expected job dictionary containing job_id,
300319
status, progress, and message for the job whose results are to be retrieved.
320+
expected_status: response HTTP status code
321+
expected_response: response body (JSON object)
301322
302323
Assertions:
303324
- Asserts that the response status code is 200 and the returned job result
@@ -306,33 +327,51 @@ async def test_get_job_result(
306327
"""
307328
# Mock app.extra to ensure 'db_table' exists
308329
mock_db_table = mocker.MagicMock()
309-
try:
310-
job_index = next(i for i, job in enumerate(mock_jobs) if job["identifier"] == expected_job["identifier"])
311-
mock_db_table.get_job.return_value = mock_jobs[job_index]
312-
except StopIteration:
313-
mock_db_table.get_job.return_value = []
330+
331+
# Simulate JobNotFoundError for non-existing jobs (HTTP 404)
332+
if expected_status == HTTP_404_NOT_FOUND:
333+
mock_db_table.get_job.side_effect = JobNotFoundError
334+
# Return an existing job normally (HTTP 200)
335+
else:
336+
mock_db_table.get_job.return_value = next(
337+
job for job in mock_jobs if job["identifier"] == expected_job["identifier"]
338+
)
339+
314340
# Patch app.extra with the mock db_table
315341
mocker.patch.object(staging_client.app, "extra", {"process_manager": mock_db_table})
316342

317343
# Call the API
318-
response = staging_client.get(f"/jobs/{expected_job['identifier']}/results")
319-
# assert response is OK and job info match, or not found for last case
320-
assert (
321-
response.status_code == HTTP_200_OK and response.json() == expected_job["status"]
322-
) or response.status_code == HTTP_404_NOT_FOUND
344+
job_id = expected_job.get("identifier")
345+
response = staging_client.get(f"/jobs/{job_id}/results")
346+
347+
# Assert response status code and content
348+
assert response.status_code == expected_status
349+
assert response.json() == expected_response
323350

324351

325352
@pytest.mark.asyncio
326353
@pytest.mark.parametrize(
327-
"expected_job",
328-
expected_jobs_test,
354+
"expected_job, expected_status, expected_response",
355+
[
356+
(
357+
{"identifier": "non_existing_id"},
358+
HTTP_404_NOT_FOUND,
359+
{"message": "Job with ID non_existing_id not found"},
360+
),
361+
*[
362+
(job, HTTP_200_OK, {"message": f"Job {job['identifier']} deleted successfully"})
363+
for job in expected_jobs_test
364+
],
365+
],
329366
)
330367
async def test_delete_job_endpoint(
331368
mocker,
332369
set_db_env_var, # pylint: disable=unused-argument
333370
staging_client,
334371
mock_jobs,
335-
expected_job, # pylint: disable=unused-argument
372+
expected_job,
373+
expected_status,
374+
expected_response,
336375
):
337376
"""
338377
Test the DELETE /jobs/{job_id} endpoint for deleting a specific job.
@@ -344,27 +383,38 @@ async def test_delete_job_endpoint(
344383
Args:
345384
mocker: A mocker object used to create mocks and patches for testing.
346385
staging_client: A test client for making requests to the FastAPI application.
386+
mock_jobs: Fixture used to mock output of tiny db jobs
347387
expected_job (dict): The expected job dictionary containing job_id,
348388
status, progress, and message for the job to be deleted.
389+
expected_status: response HTTP status code
390+
expected_response: response body (JSON object)
349391
350392
Assertions:
351393
- Asserts that the response status code is 200 if the job is successfully deleted.
352394
- Asserts that the response status code is 404 if the job does not exist.
395+
- Asserts that the response status code is 500 if other exception occurs.
353396
"""
354397
# Mock app.extra to ensure 'db_table' exists
355398
mock_db_table = mocker.MagicMock()
356-
try:
357-
job_index = next(i for i, job in enumerate(mock_jobs) if job["identifier"] == expected_job["identifier"])
358-
mock_db_table.get_job.return_value = mock_jobs[job_index]
359-
except StopIteration:
360-
mock_db_table.get_job.return_value = []
399+
400+
# Simulate JobNotFoundError for non-existing jobs (HTTP 404)
401+
if expected_status == HTTP_404_NOT_FOUND:
402+
mock_db_table.delete_job.side_effect = JobNotFoundError
403+
# Return an existing job normally (HTTP 200)
404+
else:
405+
mock_db_table.delete_job.return_value = next(
406+
job for job in mock_jobs if job["identifier"] == expected_job["identifier"]
407+
)
408+
361409
# Patch app.extra with the mock db_table
362410
mocker.patch.object(staging_client.app, "extra", {"process_manager": mock_db_table})
363411

364412
# Call the API
365413
response = staging_client.delete(f"/jobs/{expected_job['identifier']}")
366-
# assert response is OK, or not found for last case
367-
assert response.status_code in [HTTP_200_OK, HTTP_404_NOT_FOUND]
414+
415+
# Assert response status code and content
416+
assert response.status_code == expected_status
417+
assert response.json() == expected_response
368418

369419

370420
@pytest.mark.asyncio

0 commit comments

Comments
 (0)