Skip to content

Commit 4f4a736

Browse files
committed
Add tests for the revoke and list endpoints
1 parent 46a4ad2 commit 4f4a736

File tree

3 files changed

+252
-5
lines changed

3 files changed

+252
-5
lines changed

src/uos/core/orchestrator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ async def get_upload_access_grants(
241241
},
242242
)
243243
continue
244-
return grants_with_info
244+
# Sort grants by id in ascending order for predictability
245+
return sorted(grants_with_info, key=lambda x: x.id)
245246

246247
async def get_upload_box_files(
247248
self,
@@ -282,7 +283,9 @@ async def get_upload_box_files(
282283
file_ids = await self._ucs_client.get_file_upload_list(
283284
box_id=upload_box.file_upload_box_id,
284285
)
285-
return file_ids
286+
287+
# Sort files by ID for predictability
288+
return sorted(file_ids)
286289

287290
async def upsert_file_upload_box(self, file_upload_box: FileUploadBox) -> None:
288291
"""Handle FileUploadBox update events from UCS.

tests/unit/test_api.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from hexkit.utils import now_utc_ms_prec
2525

2626
from tests.fixtures import ConfigFixture
27-
from uos.core.models import ResearchDataUploadBox
27+
from uos.core.models import GrantWithBoxInfo, ResearchDataUploadBox
2828
from uos.inject import prepare_rest_app
2929
from uos.ports.inbound.orchestrator import UploadOrchestratorPort
3030
from uos.ports.outbound.http import UCSClientPort
@@ -288,15 +288,15 @@ async def test_update_research_data_upload_box(
288288
async def test_grant_upload_access(
289289
config: ConfigFixture, ds_auth_headers, user_auth_headers, bad_auth_headers
290290
):
291-
"""Test the POST /access-grant endpoint"""
291+
"""Test the POST /access-grants endpoint"""
292292
orchestrator = AsyncMock()
293293
async with (
294294
prepare_rest_app(
295295
config=config.config, upload_orchestrator_override=orchestrator
296296
) as app,
297297
AsyncTestClient(app=app) as rest_client,
298298
):
299-
url = "/access-grant"
299+
url = "/access-grants"
300300
request_data = {
301301
"user_id": str(uuid4()),
302302
"iva_id": str(uuid4()),
@@ -395,3 +395,113 @@ async def test_list_upload_box_files(
395395
orchestrator.get_upload_box_files.side_effect = TypeError()
396396
response = await rest_client.get(url, headers=user_auth_headers)
397397
assert response.status_code == 500
398+
399+
400+
async def test_revoke_upload_access_grant(
401+
config: ConfigFixture, ds_auth_headers, user_auth_headers, bad_auth_headers
402+
):
403+
"""Test the DELETE /access-grants/{grant_id} endpoint"""
404+
orchestrator = AsyncMock()
405+
test_grant_id = uuid4()
406+
407+
async with (
408+
prepare_rest_app(
409+
config=config.config, upload_orchestrator_override=orchestrator
410+
) as app,
411+
AsyncTestClient(app=app) as rest_client,
412+
):
413+
url = f"/access-grants/{test_grant_id}"
414+
415+
# unauthenticated
416+
response = await rest_client.delete(url)
417+
assert response.status_code == 403
418+
419+
# bad credentials
420+
response = await rest_client.delete(url, headers=bad_auth_headers)
421+
assert response.status_code == 401
422+
423+
# normal response but user is not a data steward (no data_steward role)
424+
response = await rest_client.delete(url, headers=user_auth_headers)
425+
assert response.status_code == 403
426+
427+
# normal response with data steward role
428+
orchestrator.revoke_upload_access_grant.return_value = None
429+
response = await rest_client.delete(url, headers=ds_auth_headers)
430+
assert response.status_code == 204
431+
432+
# handle grant not found error from core
433+
orchestrator.reset_mock()
434+
orchestrator.revoke_upload_access_grant.side_effect = (
435+
UploadOrchestratorPort.GrantNotFoundError(grant_id=test_grant_id)
436+
)
437+
response = await rest_client.delete(url, headers=ds_auth_headers)
438+
assert response.status_code == 404
439+
440+
# handle other exception
441+
orchestrator.reset_mock()
442+
orchestrator.revoke_upload_access_grant.side_effect = TypeError()
443+
response = await rest_client.delete(url, headers=ds_auth_headers)
444+
assert response.status_code == 500
445+
446+
447+
async def test_get_upload_access_grants(
448+
config: ConfigFixture, ds_auth_headers, user_auth_headers, bad_auth_headers
449+
):
450+
"""Test the GET /access-grants endpoint"""
451+
orchestrator = AsyncMock()
452+
async with (
453+
prepare_rest_app(
454+
config=config.config, upload_orchestrator_override=orchestrator
455+
) as app,
456+
AsyncTestClient(app=app) as rest_client,
457+
):
458+
url = "/access-grants"
459+
460+
# unauthenticated
461+
response = await rest_client.get(url)
462+
assert response.status_code == 403
463+
464+
# bad credentials
465+
response = await rest_client.get(url, headers=bad_auth_headers)
466+
assert response.status_code == 401
467+
468+
# normal response but user is not a data steward (no data_steward role)
469+
response = await rest_client.get(url, headers=user_auth_headers)
470+
assert response.status_code == 403
471+
472+
test_grants = [
473+
GrantWithBoxInfo(
474+
id=uuid4(),
475+
user_id=uuid4(),
476+
iva_id=uuid4(),
477+
box_id=TEST_BOX_ID,
478+
created=now_utc_ms_prec(),
479+
valid_from=now_utc_ms_prec(),
480+
valid_until=now_utc_ms_prec() + timedelta(days=7),
481+
user_name="Test User",
482+
user_email="[email protected]",
483+
user_title="Dr.",
484+
title="Test Box",
485+
description="Test box description",
486+
)
487+
]
488+
orchestrator.get_upload_access_grants.return_value = test_grants
489+
response = await rest_client.get(url, headers=ds_auth_headers)
490+
assert response.status_code == 200
491+
assert response.json() == [
492+
grant.model_dump(mode="json") for grant in test_grants
493+
]
494+
495+
# test with query parameters
496+
response = await rest_client.get(
497+
url,
498+
headers=ds_auth_headers,
499+
params={"user_id": str(uuid4()), "valid": "true"},
500+
)
501+
assert response.status_code == 200
502+
503+
# handle other exception
504+
orchestrator.reset_mock()
505+
orchestrator.get_upload_access_grants.side_effect = TypeError()
506+
response = await rest_client.get(url, headers=ds_auth_headers)
507+
assert response.status_code == 500

tests/unit/test_orchestrator.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,137 @@ async def test_get_research_data_upload_box_not_found(rig: JointRig):
386386

387387
# Verify access check was called first
388388
rig.access_client.check_box_access.assert_called_once() # type: ignore
389+
390+
391+
async def test_get_upload_access_grants_happy(rig: JointRig):
392+
"""Test the normal path for getting upload access grants."""
393+
# First create a research data upload box
394+
box_id = await rig.controller.create_research_data_upload_box(
395+
title="Test Box",
396+
description="Test Description",
397+
storage_alias="HD01",
398+
user_id=TEST_DS_ID,
399+
)
400+
401+
# Create mock upload grants that would be returned by access client
402+
test_user_id = uuid4()
403+
test_iva_id = uuid4()
404+
test_grant_id = uuid4()
405+
406+
mock_grants = [
407+
models.UploadGrant(
408+
id=test_grant_id,
409+
user_id=test_user_id,
410+
iva_id=test_iva_id,
411+
box_id=box_id,
412+
created=now_utc_ms_prec(),
413+
valid_from=now_utc_ms_prec(),
414+
valid_until=now_utc_ms_prec() + timedelta(days=7),
415+
user_name="Test User",
416+
user_email="[email protected]",
417+
user_title="Dr.",
418+
)
419+
]
420+
421+
# Mock the access client to return these grants
422+
rig.access_client.get_upload_access_grants.return_value = mock_grants # type: ignore
423+
424+
# Call the method
425+
result = await rig.controller.get_upload_access_grants(
426+
user_id=test_user_id,
427+
iva_id=test_iva_id,
428+
box_id=box_id,
429+
valid=True,
430+
)
431+
432+
# Verify the results
433+
assert len(result) == 1
434+
grant_with_info = result[0]
435+
assert grant_with_info.id == test_grant_id
436+
assert grant_with_info.user_id == test_user_id
437+
assert grant_with_info.iva_id == test_iva_id
438+
assert grant_with_info.box_id == box_id
439+
assert grant_with_info.user_name == "Test User"
440+
assert grant_with_info.user_email == "[email protected]"
441+
assert grant_with_info.user_title == "Dr."
442+
# These should come from the box
443+
assert grant_with_info.title == "Test Box"
444+
assert grant_with_info.description == "Test Description"
445+
446+
# Verify access client was called with correct parameters
447+
rig.access_client.get_upload_access_grants.assert_called_once_with( # type: ignore
448+
user_id=test_user_id,
449+
iva_id=test_iva_id,
450+
box_id=box_id,
451+
valid=True,
452+
)
453+
454+
455+
async def test_get_upload_access_grants_box_missing(rig: JointRig, caplog):
456+
"""Test the case where grants returned from the access API include a grant with
457+
a box ID that doesn't exist in the UOS. This test also checks that we emit a
458+
WARNING log (but don't raise an error).
459+
"""
460+
# Create one valid box
461+
valid_box_id = await rig.controller.create_research_data_upload_box(
462+
title="Valid Box",
463+
description="Valid Description",
464+
storage_alias="HD01",
465+
user_id=TEST_DS_ID,
466+
)
467+
468+
# Create mock upload grants - one with a valid box ID, one with an invalid box ID
469+
test_user_id = uuid4()
470+
invalid_box_id = uuid4() # This box doesn't/won't exist
471+
472+
mock_grants = [
473+
models.UploadGrant(
474+
id=uuid4(),
475+
user_id=test_user_id,
476+
iva_id=uuid4(),
477+
box_id=valid_box_id, # This box exists
478+
created=now_utc_ms_prec(),
479+
valid_from=now_utc_ms_prec(),
480+
valid_until=now_utc_ms_prec() + timedelta(days=7),
481+
user_name="Test User",
482+
user_email="[email protected]",
483+
user_title="Dr.",
484+
),
485+
models.UploadGrant(
486+
id=uuid4(),
487+
user_id=test_user_id,
488+
iva_id=uuid4(),
489+
box_id=invalid_box_id, # This box doesn't exist
490+
created=now_utc_ms_prec(),
491+
valid_from=now_utc_ms_prec(),
492+
valid_until=now_utc_ms_prec() + timedelta(days=7),
493+
user_name="Test User 2",
494+
user_email="[email protected]",
495+
user_title="Prof.",
496+
),
497+
]
498+
499+
# Mock the access client to return these grants
500+
rig.access_client.get_upload_access_grants.return_value = mock_grants # type: ignore
501+
502+
# Call the method
503+
result = await rig.controller.get_upload_access_grants()
504+
505+
# Verify the results - should only contain the grant with the valid box
506+
assert len(result) == 1
507+
grant_with_info = result[0]
508+
assert grant_with_info.box_id == valid_box_id
509+
assert grant_with_info.title == "Valid Box"
510+
assert grant_with_info.description == "Valid Description"
511+
512+
# Verify a warning was logged for the invalid box
513+
assert caplog.records
514+
warning_messages = [
515+
record.message for record in caplog.records if record.levelname == "WARNING"
516+
]
517+
assert len(warning_messages) >= 1
518+
assert any(str(invalid_box_id) in msg for msg in warning_messages)
519+
assert any("doesn't exist in UOS" in msg for msg in warning_messages)
520+
521+
# Verify access client was called
522+
rig.access_client.get_upload_access_grants.assert_called_once() # type: ignore

0 commit comments

Comments
 (0)