Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UUID checking for add_app_data_access_scope #1022

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/20240802_143010_sirosen_check_uuids.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changed
~~~~~~~

- ``TransferClient.add_app_data_access_scope`` now raises an error if it is
given an invalid collection ID. (:pr:`NUMBER`)
1 change: 1 addition & 0 deletions src/globus_sdk/services/transfer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def add_app_data_access_scope(self, collection_id: UUIDLike) -> TransferClient:

res = client.operation_ls(COLLECTION_ID)
"""
utils.check_uuid(collection_id, name="collection_id")
base_scope = Scope(TransferScopes.all)
data_access_scope = Scope(
GCSCollectionScopeBuilder(str(collection_id)).data_access,
Expand Down
32 changes: 32 additions & 0 deletions src/globus_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,38 @@ def b64str(s: str) -> str:
return b64encode(s.encode("utf-8")).decode("utf-8")


def check_uuid(s: UUIDLike, *, name: str) -> t.Literal[True]:
"""
Raise an error if the input is not a UUID

:param s: the UUID|str value
:param name: the name for this value to use in error messages

:raises TypeError: if the input is not a UUID|str
:raises ValueError: if the input is a non-UUID str

Example usage:

.. code-block:: python

def frob_it(collection_id: UUIDLike) -> Frob:
utils.check_uuid(collection_id, name="collection_id")
return Frob(collection_id)

"""
if isinstance(s, uuid.UUID):
return True
elif not isinstance(s, str):
raise TypeError(f"'{name}' must be a UUID or str (value='{s}')")

try:
uuid.UUID(s)
except ValueError as e:
raise ValueError(f"'{name}' must be a valid UUID (value='{s}')") from e

return True


def slash_join(a: str, b: str | None) -> str:
"""
Join a and b with a single slash, regardless of whether they already
Expand Down
17 changes: 11 additions & 6 deletions tests/unit/experimental/test_client_integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import uuid

import pytest

import globus_sdk
Expand Down Expand Up @@ -44,22 +46,25 @@ def test_transfer_client_default_scopes(app):
def test_transfer_client_add_app_data_access_scope(app):
client = globus_sdk.TransferClient(app=app)

client.add_app_data_access_scope("collection_id")
collection_id = str(uuid.UUID(int=0))
client.add_app_data_access_scope(collection_id)
str_list = [str(s) for s in app.get_scope_requirements("transfer.api.globus.org")]
expected = "urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/collection_id/data_access]" # noqa
expected = f"urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/{collection_id}/data_access]" # noqa: E501
assert expected in str_list


def test_transfer_client_add_app_data_access_scope_chaining(app):
collection_id_1 = str(uuid.UUID(int=1))
collection_id_2 = str(uuid.UUID(int=2))
(
globus_sdk.TransferClient(app=app)
.add_app_data_access_scope("collection_id_1")
.add_app_data_access_scope("collection_id_2")
.add_app_data_access_scope(collection_id_1)
.add_app_data_access_scope(collection_id_2)
)

str_list = [str(s) for s in app.get_scope_requirements("transfer.api.globus.org")]
expected_1 = "urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/collection_id_1/data_access]" # noqa
expected_2 = "urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/collection_id_2/data_access]" # noqa
expected_1 = f"urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/{collection_id_1}/data_access]" # noqa: E501
expected_2 = f"urn:globus:auth:scope:transfer.api.globus.org:all[*https://auth.globus.org/scopes/{collection_id_2}/data_access]" # noqa: E501
assert expected_1 in str_list
assert expected_2 in str_list

Expand Down
24 changes: 24 additions & 0 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,27 @@ def y(self_or_cls):
)
def test_safe_strseq_iter(value, expected_result):
assert list(utils.safe_strseq_iter(value)) == expected_result


@pytest.mark.parametrize("value", (uuid.UUID(int=0), str(uuid.UUID(int=1))))
def test_check_uuid_ok(value):
# no error and returns True
assert utils.check_uuid(value, name="foo")


@pytest.mark.parametrize("value", (str(uuid.UUID(int=0))[:-1], ""))
def test_check_uuid_fails_value(value):
with pytest.raises(ValueError, match="'foo' must be a valid UUID") as excinfo:
utils.check_uuid(value, name="foo")

err = excinfo.value
assert f"value='{value}'" in str(err)


@pytest.mark.parametrize("value", (object(), None, ["bar"]))
def test_check_uuid_fails_type(value):
with pytest.raises(TypeError, match="'foo' must be a UUID or str") as excinfo:
utils.check_uuid(value, name="foo")

err = excinfo.value
assert f"value='{value}'" in str(err)