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

323
.tmp_notification_models.py Normal file
View file

@ -0,0 +1,323 @@
# generated by datamodel-codegen:
# filename: commerce_notification_v1_oas3.yaml
# timestamp: 2026-04-07T06:52:28+00:00
from __future__ import annotations
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
class Config(BaseModel):
alertEmail: Optional[str] = Field(
None,
description='This field is used to add or modify an email address that will be used for Notification API alerts associated with the application. <b>getConfig</b> can be used to get the email address currently being used for alerts.',
)
class CreateSubscriptionFilterRequest(BaseModel):
filterSchema: Optional[Dict[str, Dict[str, Any]]] = Field(
None,
description='The content of a subscription filter as a valid <a href="https://json-schema.org " target="_blank">JSON Schema Core document</a> (version 2020-12 or later). The <strong>filterSchema</strong> provided must describe the subscription\'s notification payload such that it supplies valid criteria to filter the subscription\'s notifications.<br><br><span class="tablenote"><b>Note:</b> Not all topics can have filters applied to them. Use <a href="/api-docs/commerce/notification/resources/topic/methods/getTopic">getTopic</a> and <a href="/api-docs/commerce/notification/resources/topic/methods/getTopics">getTopics</a> requests to determine if a specific topic is filterable. Filterable topics have the boolean <b>filterable</b> returned as <code>true</code> in the response.</span><br><span class="tablenote"><b>Note:</b> If the JSON supplied as a subscription filter specifies a field that does not exist in the notifications for a topic, or if the topic is not filterable, the filter will be rejected and become <strong>DISABLED</strong>. If it is valid, however, the filter will move from <strong>PENDING</strong> status to <strong>ENABLED</strong> status.</span><br>Initially, when the <b>createSubscriptionFilter</b> request has been made, if the request has a valid JSON body a <b>201&nbsp;Created</b> is returned. After that, the validation of the <b>filterSchema</b> happens. See <a href="/api-docs/commerce/notification/overview.html#create-filter" target="_blank">Creating a subscription filter for a topic</a> for additional information.',
)
class DeliveryConfig(BaseModel):
endpoint: Optional[str] = Field(
None,
description='The endpoint for this destination.<br><br><span class="tablenote"><b>Note:</b> The provided endpoint URL should use the HTTPS protocol, and it should not contain an internal IP address or <code>localhost</code> in its path.</span>',
)
verificationToken: Optional[str] = Field(
None,
description='The verification token associated with this endpoint.<br><br><span class="tablenote"><b>Note:</b> The provided verification token must be between 32 and 80 characters. Allowed characters include alphanumeric characters, underscores (<code>_</code>), and hyphens (<code>-</code>); no other characters are allowed.</span>',
)
class Destination(BaseModel):
deliveryConfig: Optional[DeliveryConfig] = Field(
None, description='The configuration associated with this destination.'
)
destinationId: Optional[str] = Field(
None, description='The unique identifier for the destination.'
)
name: Optional[str] = Field(
None, description='The name associated with this destination.'
)
status: Optional[str] = Field(
None,
description='The status for this destination.<br><br><span class="tablenote"><b>Note:</b> The <b>MARKED_DOWN</b> value is set by eBay systems and cannot be used in a create or update call by applications.</span><br><br><b>Valid values:</b><ul><li><code>ENABLED</code></li><li><code>DISABLED</code></li><li><code>MARKED_DOWN</code></li></ul> For implementation help, refer to <a href=\'https://developer.ebay.com/api-docs/commerce/notification/types/api:DestinationStatusEnum\'>eBay API documentation</a>',
)
class DestinationRequest(BaseModel):
deliveryConfig: Optional[DeliveryConfig] = Field(
None,
description='This container is used to specify the destination endpoint and verification token associated with this endpoint.',
)
name: Optional[str] = Field(
None, description='The seller-specified name for the destination endpoint.'
)
status: Optional[str] = Field(
None,
description='This field sets the status for the destination endpoint as <code>ENABLED</code> or <code>DISABLED</code>.<br><br><span class="tablenote"><b>Note:</b> The <b>MARKED_DOWN</b> value is set by eBay systems and cannot be used in a create or update call by applications.</span> For implementation help, refer to <a href=\'https://developer.ebay.com/api-docs/commerce/notification/types/api:DestinationStatusEnum\'>eBay API documentation</a>',
)
class DestinationSearchResponse(BaseModel):
destinations: Optional[List[Destination]] = Field(
None, description='An array that contains the destination details.'
)
href: Optional[str] = Field(
None,
description='The path to the call URI that produced the current page of results.',
)
limit: Optional[int] = Field(
None,
description='The number of records to show in the current response.<br><br><b>Default:</b> 20',
)
next: Optional[str] = Field(
None,
description='The URL to access the next set of results. This field includes a <strong>continuation_token</strong>. No <b>prev</b> field is returned, but this value is persistent during the session so that you can use it to return to the next page.<br><br>This field is not returned if fewer records than specified by the <strong>limit</strong> field are returned.',
)
total: Optional[int] = Field(
None, description='The total number of matches for the search criteria.'
)
class ErrorParameter(BaseModel):
name: Optional[str] = Field(None, description='The object of the error.')
value: Optional[str] = Field(None, description='The value of the object.')
class PayloadDetail(BaseModel):
deliveryProtocol: Optional[str] = Field(
None,
description="The supported delivery protocols. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:ProtocolEnum'>eBay API documentation</a>",
)
deprecated: Optional[bool] = Field(None, description='A deprecation indicator.')
format: Optional[List[str]] = Field(
None,
description='The supported format. Presently, <code>JSON</code> is the only supported format.',
)
schemaVersion: Optional[str] = Field(
None, description='The supported schema version.'
)
class PublicKey(BaseModel):
algorithm: Optional[str] = Field(
None,
description='The algorithm associated with the public key that is returned, such as Elliptic Curve Digital Signature Algorithm (ECDSA).',
)
digest: Optional[str] = Field(
None,
description='The digest associated with the public key that is returned, such as Secure Hash Algorithm 1 (SHA1).',
)
key: Optional[str] = Field(
None,
description='The public key that is returned for the specified key ID.<br><br>This value is used to validate the eBay push notification message payload.',
)
class SubscriptionFilter(BaseModel):
creationDate: Optional[str] = Field(
None, description='The creation date for this subscription filter.'
)
filterId: Optional[str] = Field(
None, description='The unique identifier for this subscription filter.'
)
filterSchema: Optional[Dict[str, Dict[str, Any]]] = Field(
None,
description='The content of this subscription filter as a valid <a href="https://json-schema.org " target="_blank">JSON Schema Core document</a> (version 2020-12 or later). The <strong>filterSchema</strong> provided must describe the subscription\'s notification payload such that it supplies valid criteria to filter the subscription\'s notifications.',
)
filterStatus: Optional[str] = Field(
None,
description="The status of this subscription filter. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:SubscriptionFilterStatus'>eBay API documentation</a>",
)
subscriptionId: Optional[str] = Field(
None, description='The unique identifier for the subscription.'
)
class SubscriptionPayloadDetail(BaseModel):
deliveryProtocol: Optional[str] = Field(
None,
description='The supported delivery protocol of the notification topic.<br><br><span class="tablenote"><b>Note:</b> <code>HTTPS</code> is currently the only supported delivery protocol of all notification topics. </span> For implementation help, refer to <a href=\'https://developer.ebay.com/api-docs/commerce/notification/types/api:ProtocolEnum\'>eBay API documentation</a>',
)
format: Optional[str] = Field(
None,
description='The supported data format of the payload.<br><br><span class="tablenote"><b>Note:</b> JSON is currently the only supported format for all notification topics.</span> For implementation help, refer to <a href=\'https://developer.ebay.com/api-docs/commerce/notification/types/api:FormatTypeEnum\'>eBay API documentation</a>',
)
schemaVersion: Optional[str] = Field(
None,
description='The supported schema version for the notification topic. See the <b>supportedPayloads.schemaVersion</b> field for the topic in <b>getTopics</b> or <b>getTopic</b> response.',
)
class Topic(BaseModel):
authorizationScopes: Optional[List[str]] = Field(
None,
description='The authorization scopes required to subscribe to this topic.',
)
context: Optional[str] = Field(
None,
description="The business context associated with this topic. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:ContextEnum'>eBay API documentation</a>",
)
description: Optional[str] = Field(
None, description='The description of the topic.'
)
filterable: Optional[bool] = Field(
None, description='The indicator of whether this topic is filterable or not.'
)
scope: Optional[str] = Field(
None,
description="The scope of this topic. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:ScopeEnum'>eBay API documentation</a>",
)
status: Optional[str] = Field(
None,
description="The status of this topic. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:StatusEnum'>eBay API documentation</a>",
)
supportedPayloads: Optional[List[PayloadDetail]] = Field(
None, description='The supported payloads for this topic.'
)
topicId: Optional[str] = Field(
None, description='The unique identifier for the topic.'
)
class TopicSearchResponse(BaseModel):
href: Optional[str] = Field(
None,
description='The path to the call URI that produced the current page of results.',
)
limit: Optional[int] = Field(
None,
description='The value of the limit parameter submitted in the request, which is the maximum number of items to return per page, from the result set. A result set is the complete set of results returned by the method.<br><br><span class="tablenote"><b>Note:</b> Though this parameter is not required to be submitted in the request, the parameter defaults to <code>20</code> if omitted.</span>',
)
next: Optional[str] = Field(
None,
description='The URL to access the next set of results. This field includes a <strong>continuation_token</strong>. No <b>prev</b> field is returned, but this value is persistent during the session so that you can use it to return to the next page.<br><br>This field is not returned if fewer records than specified by the <strong>limit</strong> field are returned.',
)
topics: Optional[List[Topic]] = Field(
None, description='An array of topics that match the specified criteria.'
)
total: Optional[int] = Field(
None, description='The total number of matches for the search criteria.'
)
class UpdateSubscriptionRequest(BaseModel):
destinationId: Optional[str] = Field(
None,
description='The unique identifier of the destination endpoint that will receive notifications associated with this subscription. Use <b>getDestinations</b> to retrieve destination IDs.',
)
payload: Optional[SubscriptionPayloadDetail] = Field(
None, description='The payload associated with this subscription.'
)
status: Optional[str] = Field(
None,
description="Set the status of the subscription being updated to ENABLED or DISABLED. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:SubscriptionStatusEnum'>eBay API documentation</a>",
)
class CreateSubscriptionRequest(BaseModel):
destinationId: Optional[str] = Field(
None,
description='The unique identifier of the destination endpoint that will receive notifications associated with this subscription. Use the <b>getDestinations</b> method to retrieve destination IDs.',
)
payload: Optional[SubscriptionPayloadDetail] = Field(
None,
description='The payload associated with the notification topic. Use <b>getTopics</b> or <b>getTopic</b> to get the supported payload for the topic.',
)
status: Optional[str] = Field(
None,
description="Set the status of the subscription to <code>ENABLED</code> or <code>DISABLED</code>. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:SubscriptionStatusEnum'>eBay API documentation</a>",
)
topicId: Optional[str] = Field(
None,
description='The unique identifier of the notification topic to subscribe to. Use <b>getTopics</b> to get topic IDs.',
)
class Error(BaseModel):
category: Optional[str] = Field(None, description='Identifies the type of erro.')
domain: Optional[str] = Field(
None,
description='Name for the primary system where the error occurred. This is relevant for application errors.',
)
errorId: Optional[int] = Field(
None, description='A unique number to identify the error.'
)
inputRefIds: Optional[List[str]] = Field(
None,
description='An array of request elements most closely associated to the error.',
)
longMessage: Optional[str] = Field(
None, description='A more detailed explanation of the error.'
)
message: Optional[str] = Field(
None,
description="Information on how to correct the problem, in the end user's terms and language where applicable.",
)
outputRefIds: Optional[List[str]] = Field(
None,
description='An array of request elements most closely associated to the error.',
)
parameters: Optional[List[ErrorParameter]] = Field(
None,
description='An array of name/value pairs that describe details the error condition. These are useful when multiple errors are returned.',
)
subdomain: Optional[str] = Field(
None,
description='Further helps indicate which subsystem the error is coming from. System subcategories include: Initialization, Serialization, Security, Monitoring, Rate Limiting, etc.',
)
class Subscription(BaseModel):
creationDate: Optional[str] = Field(
None, description='The creation date for this subscription.'
)
destinationId: Optional[str] = Field(
None,
description='The unique identifier for the destination associated with this subscription.',
)
filterId: Optional[str] = Field(
None,
description='The unique identifier for the filter associated with this subscription.',
)
payload: Optional[SubscriptionPayloadDetail] = Field(
None, description='The payload associated with this subscription.'
)
status: Optional[str] = Field(
None,
description="The status of this subscription. For implementation help, refer to <a href='https://developer.ebay.com/api-docs/commerce/notification/types/api:SubscriptionStatusEnum'>eBay API documentation</a>",
)
subscriptionId: Optional[str] = Field(
None, description='The unique identifier for the subscription.'
)
topicId: Optional[str] = Field(
None,
description='The unique identifier for the topic associated with this subscription.',
)
class SubscriptionSearchResponse(BaseModel):
href: Optional[str] = Field(
None,
description='The path to the call URI that produced the current page of results.',
)
limit: Optional[int] = Field(
None,
description='The value of the limit parameter submitted in the request, which is the maximum number of items to return per page, from the result set. A result set is the complete set of results returned by the method.<br><br><span class="tablenote"><b>Note:</b> Though this parameter is not required to be submitted in the request, the parameter defaults to <code>20</code> if omitted.</span><br><br><b>Default:</b> 20',
)
next: Optional[str] = Field(
None,
description='The URL to access the next set of results. This field includes a <strong>continuation_token</strong>. No <b>prev</b> field is returned, but this value is persistent during the session so that you can use it to return to the next page.<br><br>This field is not returned if fewer records than specified by the <strong>limit</strong> field are returned.',
)
subscriptions: Optional[List[Subscription]] = Field(
None, description='The subscriptions that match the search criteria.'
)
total: Optional[int] = Field(
None, description='The total number of matches for the search criteria.'
)

View file

@ -11,6 +11,7 @@ from ebay_client.generated.account.models import (
ACCOUNT_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.account"
ACCOUNT_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.account.readonly"
ACCOUNT_READ_SCOPE_OPTIONS = [[ACCOUNT_READ_SCOPE], [ACCOUNT_SCOPE]]
class AccountClient:
@ -22,7 +23,7 @@ class AccountClient:
FulfillmentPolicyResponse,
"GET",
"/sell/account/v1/fulfillment_policy",
scopes=[ACCOUNT_READ_SCOPE],
scope_options=ACCOUNT_READ_SCOPE_OPTIONS,
params={"marketplace_id": marketplace_id},
)
@ -31,7 +32,7 @@ class AccountClient:
PaymentPolicyResponse,
"GET",
"/sell/account/v1/payment_policy",
scopes=[ACCOUNT_READ_SCOPE],
scope_options=ACCOUNT_READ_SCOPE_OPTIONS,
params={"marketplace_id": marketplace_id},
)
@ -40,7 +41,7 @@ class AccountClient:
ReturnPolicyResponse,
"GET",
"/sell/account/v1/return_policy",
scopes=[ACCOUNT_READ_SCOPE],
scope_options=ACCOUNT_READ_SCOPE_OPTIONS,
params={"marketplace_id": marketplace_id},
)
@ -49,7 +50,7 @@ class AccountClient:
SellingPrivileges,
"GET",
"/sell/account/v1/privilege",
scopes=[ACCOUNT_SCOPE],
scope_options=ACCOUNT_READ_SCOPE_OPTIONS,
)
def get_opted_in_programs(self) -> Programs:
@ -57,5 +58,5 @@ class AccountClient:
Programs,
"GET",
"/sell/account/v1/program/get_opted_in_programs",
scopes=[ACCOUNT_READ_SCOPE],
scope_options=ACCOUNT_READ_SCOPE_OPTIONS,
)

View file

@ -10,6 +10,16 @@ from ebay_client.generated.feed.models import (
FEED_INVENTORY_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"
FEED_FULFILLMENT_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.fulfillment"
FEED_MARKETING_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.marketing"
FEED_CATALOG_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/commerce.catalog.readonly"
FEED_ANALYTICS_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.analytics.readonly"
FEED_READ_SCOPE_OPTIONS = [
[FEED_INVENTORY_SCOPE],
[FEED_FULFILLMENT_SCOPE],
[FEED_MARKETING_SCOPE],
[FEED_CATALOG_READ_SCOPE],
[FEED_ANALYTICS_READ_SCOPE],
]
class FeedClient:
@ -21,7 +31,7 @@ class FeedClient:
TaskCollection,
"GET",
"/sell/feed/v1/task",
scopes=[FEED_INVENTORY_SCOPE, FEED_FULFILLMENT_SCOPE],
scope_options=FEED_READ_SCOPE_OPTIONS,
params={"feed_type": feed_type},
)
@ -30,7 +40,7 @@ class FeedClient:
Task,
"GET",
f"/sell/feed/v1/task/{task_id}",
scopes=[FEED_INVENTORY_SCOPE, FEED_FULFILLMENT_SCOPE],
scope_options=FEED_READ_SCOPE_OPTIONS,
)
def get_schedule_templates(self) -> ScheduleTemplateCollection:
@ -38,7 +48,7 @@ class FeedClient:
ScheduleTemplateCollection,
"GET",
"/sell/feed/v1/schedule_template",
scopes=[FEED_INVENTORY_SCOPE, FEED_FULFILLMENT_SCOPE],
scope_options=FEED_READ_SCOPE_OPTIONS,
)
def get_schedules(self) -> UserScheduleCollection:
@ -46,5 +56,5 @@ class FeedClient:
UserScheduleCollection,
"GET",
"/sell/feed/v1/schedule",
scopes=[FEED_INVENTORY_SCOPE, FEED_FULFILLMENT_SCOPE],
scope_options=FEED_READ_SCOPE_OPTIONS,
)

View file

@ -8,7 +8,9 @@ from ebay_client.generated.fulfillment.models import (
ShippingFulfillmentPagedCollection,
)
FULFILLMENT_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.fulfillment"
FULFILLMENT_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly"
FULFILLMENT_READ_SCOPE_OPTIONS = [[FULFILLMENT_READ_SCOPE], [FULFILLMENT_SCOPE]]
class FulfillmentClient:
@ -20,7 +22,7 @@ class FulfillmentClient:
Order,
"GET",
f"/sell/fulfillment/v1/order/{order_id}",
scopes=[FULFILLMENT_READ_SCOPE],
scope_options=FULFILLMENT_READ_SCOPE_OPTIONS,
)
def get_orders(self, *, limit: int | None = None, offset: int | None = None) -> OrderSearchPagedCollection:
@ -28,7 +30,7 @@ class FulfillmentClient:
OrderSearchPagedCollection,
"GET",
"/sell/fulfillment/v1/order",
scopes=[FULFILLMENT_READ_SCOPE],
scope_options=FULFILLMENT_READ_SCOPE_OPTIONS,
params={"limit": limit, "offset": offset},
)
@ -37,7 +39,7 @@ class FulfillmentClient:
ShippingFulfillmentPagedCollection,
"GET",
f"/sell/fulfillment/v1/order/{order_id}/shipping_fulfillment",
scopes=[FULFILLMENT_READ_SCOPE],
scope_options=FULFILLMENT_READ_SCOPE_OPTIONS,
)
def get_shipping_fulfillment(self, order_id: str, fulfillment_id: str) -> ShippingFulfillment:
@ -45,5 +47,5 @@ class FulfillmentClient:
ShippingFulfillment,
"GET",
f"/sell/fulfillment/v1/order/{order_id}/shipping_fulfillment/{fulfillment_id}",
scopes=[FULFILLMENT_READ_SCOPE],
scope_options=FULFILLMENT_READ_SCOPE_OPTIONS,
)

View file

@ -8,7 +8,9 @@ from ebay_client.generated.inventory.models import (
Offers,
)
INVENTORY_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"
INVENTORY_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory.readonly"
INVENTORY_READ_SCOPE_OPTIONS = [[INVENTORY_READ_SCOPE], [INVENTORY_SCOPE]]
class InventoryClient:
@ -20,7 +22,7 @@ class InventoryClient:
InventoryItemWithSkuLocaleGroupid,
"GET",
f"/sell/inventory/v1/inventory_item/{sku}",
scopes=[INVENTORY_READ_SCOPE],
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def get_inventory_items(self, *, limit: int | None = None, offset: int | None = None) -> InventoryItems:
@ -28,7 +30,7 @@ class InventoryClient:
InventoryItems,
"GET",
"/sell/inventory/v1/inventory_item",
scopes=[INVENTORY_READ_SCOPE],
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
params={"limit": limit, "offset": offset},
)
@ -37,7 +39,7 @@ class InventoryClient:
OfferResponseWithListingId,
"GET",
f"/sell/inventory/v1/offer/{offer_id}",
scopes=[INVENTORY_READ_SCOPE],
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
)
def get_offers(self, *, limit: int | None = None, offset: int | None = None, sku: str | None = None) -> Offers:
@ -45,6 +47,6 @@ class InventoryClient:
Offers,
"GET",
"/sell/inventory/v1/offer",
scopes=[INVENTORY_READ_SCOPE],
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
params={"limit": limit, "offset": offset, "sku": sku},
)

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",
@ -268,3 +285,78 @@ 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"
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"],
]