Refactor API client scope handling to support multiple scope options across account, feed, fulfillment, and inventory clients; add tests for scope validation

This commit is contained in:
claudi 2026-04-07 09:57:34 +02:00
parent e86ed4fbac
commit e99937cc43
6 changed files with 448 additions and 18 deletions

View file

@ -49,6 +49,23 @@ def build_transport() -> ApiTransport:
return ApiTransport(base_url="https://api.ebay.com", oauth_client=DummyOAuthClient())
class RecordingOAuthClient:
def __init__(self) -> None:
self.last_scopes: list[str] | None = None
self.last_scope_options: list[list[str]] | None = None
def get_valid_token(
self,
*,
scopes: list[str] | None = None,
scope_options: list[list[str]] | None = None,
) -> OAuthToken:
self.last_scopes = scopes
self.last_scope_options = scope_options
resolved_scopes = scopes or (scope_options[0] if scope_options else [])
return OAuthToken(access_token="recorded-token", scope=" ".join(resolved_scopes))
def test_notification_wrapper_returns_pydantic_model(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
method="GET",
@ -267,4 +284,79 @@ def test_feed_wrapper_returns_task_collection_model(httpx_mock: HTTPXMock) -> No
assert isinstance(result, TaskCollection)
assert result.total == 1
assert result.tasks and result.tasks[0].taskId == "TASK-1"
assert result.tasks and result.tasks[0].taskId == "TASK-1"
def test_inventory_wrapper_accepts_readonly_or_full_scope_options(httpx_mock: HTTPXMock) -> None:
oauth_client = RecordingOAuthClient()
transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
json={"sku": "SKU-1"},
)
InventoryClient(transport).get_inventory_item("SKU-1")
assert oauth_client.last_scopes is None
assert oauth_client.last_scope_options == [
["https://api.ebay.com/oauth/api_scope/sell.inventory.readonly"],
["https://api.ebay.com/oauth/api_scope/sell.inventory"],
]
def test_fulfillment_wrapper_accepts_readonly_or_full_scope_options(httpx_mock: HTTPXMock) -> None:
oauth_client = RecordingOAuthClient()
transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1",
json={"orderId": "ORDER-1"},
)
FulfillmentClient(transport).get_order("ORDER-1")
assert oauth_client.last_scopes is None
assert oauth_client.last_scope_options == [
["https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly"],
["https://api.ebay.com/oauth/api_scope/sell.fulfillment"],
]
def test_account_wrapper_accepts_readonly_or_full_scope_options(httpx_mock: HTTPXMock) -> None:
oauth_client = RecordingOAuthClient()
transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/account/v1/privilege",
json={},
)
AccountClient(transport).get_privileges()
assert oauth_client.last_scopes is None
assert oauth_client.last_scope_options == [
["https://api.ebay.com/oauth/api_scope/sell.account.readonly"],
["https://api.ebay.com/oauth/api_scope/sell.account"],
]
def test_feed_wrapper_accepts_any_documented_feed_scope_option(httpx_mock: HTTPXMock) -> None:
oauth_client = RecordingOAuthClient()
transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client)
httpx_mock.add_response(
method="GET",
url="https://api.ebay.com/sell/feed/v1/task?feed_type=LMS_ORDER_REPORT",
json={"tasks": [{"taskId": "TASK-1"}], "total": 1},
)
FeedClient(transport).get_tasks(feed_type="LMS_ORDER_REPORT")
assert oauth_client.last_scopes is None
assert oauth_client.last_scope_options == [
["https://api.ebay.com/oauth/api_scope/sell.inventory"],
["https://api.ebay.com/oauth/api_scope/sell.fulfillment"],
["https://api.ebay.com/oauth/api_scope/sell.marketing"],
["https://api.ebay.com/oauth/api_scope/commerce.catalog.readonly"],
["https://api.ebay.com/oauth/api_scope/sell.analytics.readonly"],
]