523 lines
No EOL
19 KiB
Python
523 lines
No EOL
19 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from pytest_httpx import HTTPXMock
|
|
|
|
from ebay_client.account.client import AccountClient
|
|
from ebay_client.core.auth.models import OAuthToken
|
|
from ebay_client.core.http.transport import ApiTransport
|
|
from ebay_client.feed.client import FeedClient
|
|
from ebay_client.fulfillment.client import FulfillmentClient
|
|
from ebay_client.generated.account.models import Programs
|
|
from ebay_client.generated.feed.models import TaskCollection
|
|
from ebay_client.generated.fulfillment.models import Order
|
|
from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGroupid
|
|
from ebay_client.generated.media.models import (
|
|
CreateDocumentFromUrlRequest,
|
|
CreateDocumentRequest,
|
|
CreateImageFromUrlRequest,
|
|
CreateVideoRequest,
|
|
DocumentResponse,
|
|
ImageResponse,
|
|
Video,
|
|
)
|
|
from ebay_client.generated.notification.models import (
|
|
Config,
|
|
CreateSubscriptionFilterRequest,
|
|
CreateSubscriptionRequest,
|
|
DeliveryConfig,
|
|
Subscription,
|
|
SubscriptionFilter,
|
|
SubscriptionPayloadDetail,
|
|
DestinationRequest,
|
|
TopicSearchResponse,
|
|
UpdateSubscriptionRequest,
|
|
)
|
|
from ebay_client.inventory.client import InventoryClient
|
|
from ebay_client.media.client import CreatedMediaResource, MediaClient
|
|
from ebay_client.notification.client import NotificationClient
|
|
|
|
|
|
class DummyOAuthClient:
|
|
def get_valid_token(
|
|
self,
|
|
*,
|
|
scopes: list[str] | None = None,
|
|
scope_options: list[list[str]] | None = None,
|
|
) -> OAuthToken:
|
|
if scopes:
|
|
resolved_scopes = scopes
|
|
elif scope_options:
|
|
resolved_scopes = scope_options[0]
|
|
else:
|
|
resolved_scopes = []
|
|
return OAuthToken(access_token="test-token", scope=" ".join(resolved_scopes))
|
|
|
|
|
|
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",
|
|
url="https://api.ebay.com/commerce/notification/v1/topic?limit=10",
|
|
json={"topics": [{"topicId": "MARKETPLACE_ACCOUNT_DELETION", "description": "topic"}], "total": 1},
|
|
)
|
|
|
|
client = NotificationClient(build_transport())
|
|
result = client.get_topics(limit=10)
|
|
|
|
assert isinstance(result, TopicSearchResponse)
|
|
assert result.total == 1
|
|
assert result.topics and result.topics[0].topicId == "MARKETPLACE_ACCOUNT_DELETION"
|
|
request = httpx_mock.get_requests()[0]
|
|
assert request.headers["Authorization"] == "Bearer test-token"
|
|
|
|
|
|
def test_notification_wrapper_serializes_typed_request_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/notification/v1/destination",
|
|
json={},
|
|
status_code=201,
|
|
)
|
|
|
|
client = NotificationClient(build_transport())
|
|
payload = DestinationRequest(
|
|
name="main-destination",
|
|
status="ENABLED",
|
|
deliveryConfig=DeliveryConfig(
|
|
endpoint="https://example.test/webhook",
|
|
verificationToken="verification_token_1234567890123456",
|
|
),
|
|
)
|
|
|
|
client.create_destination(payload)
|
|
|
|
request = httpx_mock.get_requests()[0]
|
|
body = json.loads(request.content.decode("utf-8"))
|
|
assert body["name"] == "main-destination"
|
|
assert body["deliveryConfig"]["endpoint"] == "https://example.test/webhook"
|
|
|
|
|
|
def test_notification_config_wrapper_round_trip(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/commerce/notification/v1/config",
|
|
json={"alertEmail": "alerts@example.test"},
|
|
)
|
|
httpx_mock.add_response(
|
|
method="PUT",
|
|
url="https://api.ebay.com/commerce/notification/v1/config",
|
|
status_code=204,
|
|
)
|
|
|
|
client = NotificationClient(build_transport())
|
|
config = client.get_config()
|
|
client.update_config(Config(alertEmail="ops@example.test"))
|
|
|
|
assert isinstance(config, Config)
|
|
assert config.alertEmail == "alerts@example.test"
|
|
request = httpx_mock.get_requests()[1]
|
|
body = json.loads(request.content.decode("utf-8"))
|
|
assert body["alertEmail"] == "ops@example.test"
|
|
|
|
|
|
def test_notification_subscription_wrapper_serializes_and_returns_models(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription",
|
|
json={},
|
|
status_code=201,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1",
|
|
json={"subscriptionId": "SUB-1", "topicId": "TOPIC-1", "status": "ENABLED"},
|
|
)
|
|
httpx_mock.add_response(
|
|
method="PUT",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1",
|
|
status_code=204,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/enable",
|
|
status_code=204,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/test",
|
|
status_code=202,
|
|
)
|
|
|
|
client = NotificationClient(build_transport())
|
|
create_payload = CreateSubscriptionRequest(
|
|
destinationId="DEST-1",
|
|
topicId="TOPIC-1",
|
|
status="DISABLED",
|
|
payload=SubscriptionPayloadDetail(
|
|
deliveryProtocol="HTTPS",
|
|
format="JSON",
|
|
schemaVersion="1.0",
|
|
),
|
|
)
|
|
update_payload = UpdateSubscriptionRequest(status="ENABLED", destinationId="DEST-1")
|
|
|
|
client.create_subscription(create_payload)
|
|
subscription = client.get_subscription("SUB-1")
|
|
client.update_subscription("SUB-1", update_payload)
|
|
client.enable_subscription("SUB-1")
|
|
client.test_subscription("SUB-1")
|
|
|
|
assert isinstance(subscription, Subscription)
|
|
assert subscription.subscriptionId == "SUB-1"
|
|
create_request = httpx_mock.get_requests()[0]
|
|
create_body = json.loads(create_request.content.decode("utf-8"))
|
|
assert create_body["destinationId"] == "DEST-1"
|
|
assert create_body["payload"]["deliveryProtocol"] == "HTTPS"
|
|
update_request = httpx_mock.get_requests()[2]
|
|
update_body = json.loads(update_request.content.decode("utf-8"))
|
|
assert update_body == {"destinationId": "DEST-1", "status": "ENABLED"}
|
|
|
|
|
|
def test_notification_subscription_filter_wrapper_serializes_and_returns_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter",
|
|
json={},
|
|
status_code=201,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter/FILTER-1",
|
|
json={"filterId": "FILTER-1", "subscriptionId": "SUB-1", "filterStatus": "ENABLED"},
|
|
)
|
|
httpx_mock.add_response(
|
|
method="DELETE",
|
|
url="https://api.ebay.com/commerce/notification/v1/subscription/SUB-1/filter/FILTER-1",
|
|
status_code=204,
|
|
)
|
|
|
|
client = NotificationClient(build_transport())
|
|
payload = CreateSubscriptionFilterRequest(
|
|
filterSchema={
|
|
"properties": {
|
|
"data": {
|
|
"type": "object",
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
client.create_subscription_filter("SUB-1", payload)
|
|
subscription_filter = client.get_subscription_filter("SUB-1", "FILTER-1")
|
|
client.delete_subscription_filter("SUB-1", "FILTER-1")
|
|
|
|
assert isinstance(subscription_filter, SubscriptionFilter)
|
|
assert subscription_filter.filterId == "FILTER-1"
|
|
request = httpx_mock.get_requests()[0]
|
|
body = json.loads(request.content.decode("utf-8"))
|
|
assert body["filterSchema"]["properties"]["data"]["type"] == "object"
|
|
|
|
|
|
def test_inventory_wrapper_returns_inventory_item_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
|
|
json={"sku": "SKU-1"},
|
|
)
|
|
|
|
client = InventoryClient(build_transport())
|
|
result = client.get_inventory_item("SKU-1")
|
|
|
|
assert isinstance(result, InventoryItemWithSkuLocaleGroupid)
|
|
assert result.sku == "SKU-1"
|
|
|
|
|
|
def test_fulfillment_wrapper_returns_order_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1",
|
|
json={"orderId": "ORDER-1"},
|
|
)
|
|
|
|
client = FulfillmentClient(build_transport())
|
|
result = client.get_order("ORDER-1")
|
|
|
|
assert isinstance(result, Order)
|
|
assert result.orderId == "ORDER-1"
|
|
|
|
|
|
def test_account_wrapper_returns_programs_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/sell/account/v1/program/get_opted_in_programs",
|
|
json={"programs": [{"programType": "OUT_OF_STOCK_CONTROL"}]},
|
|
)
|
|
|
|
client = AccountClient(build_transport())
|
|
result = client.get_opted_in_programs()
|
|
|
|
assert isinstance(result, Programs)
|
|
assert result.programs and result.programs[0].programType == "OUT_OF_STOCK_CONTROL"
|
|
|
|
|
|
def test_feed_wrapper_returns_task_collection_model(httpx_mock: HTTPXMock) -> None:
|
|
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},
|
|
)
|
|
|
|
client = FeedClient(build_transport())
|
|
result = client.get_tasks(feed_type="LMS_ORDER_REPORT")
|
|
|
|
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"],
|
|
]
|
|
|
|
|
|
def test_media_wrapper_returns_image_model_from_url(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/image/create_image_from_url",
|
|
json={"imageUrl": "https://i.ebayimg.com/images/g/demo.jpg", "expirationDate": "2026-12-31T00:00:00Z"},
|
|
status_code=201,
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
result = client.create_image_from_url(
|
|
CreateImageFromUrlRequest(imageUrl="https://example.test/demo.jpg")
|
|
)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
assert result.imageUrl == "https://i.ebayimg.com/images/g/demo.jpg"
|
|
request = httpx_mock.get_requests()[0]
|
|
body = json.loads(request.content.decode("utf-8"))
|
|
assert body["imageUrl"] == "https://example.test/demo.jpg"
|
|
|
|
|
|
def test_media_wrapper_serializes_multipart_image_upload(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/image/create_image_from_file",
|
|
json={"imageUrl": "https://i.ebayimg.com/images/g/uploaded.jpg"},
|
|
status_code=201,
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
result = client.create_image_from_file(
|
|
file_name="demo.jpg",
|
|
content=b"binary-image-content",
|
|
content_type="image/jpeg",
|
|
)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
request = httpx_mock.get_requests()[0]
|
|
assert request.headers["Content-Type"].startswith("multipart/form-data;")
|
|
assert b"name=\"image\"" in request.content
|
|
assert b"filename=\"demo.jpg\"" in request.content
|
|
assert b"binary-image-content" in request.content
|
|
|
|
|
|
def test_media_wrapper_returns_created_resource_location_for_video(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/video",
|
|
status_code=201,
|
|
headers={"Location": "https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1"},
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
result = client.create_video(
|
|
CreateVideoRequest(title="Demo", size=1024, classification=["ITEM"])
|
|
)
|
|
|
|
assert isinstance(result, CreatedMediaResource)
|
|
assert result.location == "https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1"
|
|
|
|
|
|
def test_media_wrapper_returns_video_model(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1",
|
|
json={"videoId": "VIDEO-1", "status": "LIVE", "title": "Demo"},
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
result = client.get_video("VIDEO-1")
|
|
|
|
assert isinstance(result, Video)
|
|
assert result.videoId == "VIDEO-1"
|
|
|
|
|
|
def test_media_wrapper_uploads_video_bytes(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1/upload",
|
|
status_code=200,
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
client.upload_video(
|
|
"VIDEO-1",
|
|
content=b"video-bytes",
|
|
content_length=11,
|
|
content_range="bytes 0-10/11",
|
|
)
|
|
|
|
request = httpx_mock.get_requests()[0]
|
|
assert request.headers["Content-Type"] == "application/octet-stream"
|
|
assert request.headers["Content-Length"] == "11"
|
|
assert request.headers["Content-Range"] == "bytes 0-10/11"
|
|
assert request.content == b"video-bytes"
|
|
|
|
|
|
def test_media_wrapper_returns_document_models(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/document",
|
|
json={"documentId": "DOC-1", "documentStatus": "PENDING_UPLOAD"},
|
|
status_code=201,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/document/create_document_from_url",
|
|
json={"documentId": "DOC-2", "documentStatus": "SUBMITTED"},
|
|
status_code=201,
|
|
)
|
|
httpx_mock.add_response(
|
|
method="GET",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/document/DOC-1",
|
|
json={"documentId": "DOC-1", "documentStatus": "ACCEPTED", "documentType": "USER_GUIDE_OR_MANUAL"},
|
|
)
|
|
httpx_mock.add_response(
|
|
method="POST",
|
|
url="https://api.ebay.com/commerce/media/v1_beta/document/DOC-1/upload",
|
|
json={"documentId": "DOC-1", "documentStatus": "SUBMITTED"},
|
|
status_code=200,
|
|
)
|
|
|
|
client = MediaClient(build_transport())
|
|
created = client.create_document(
|
|
CreateDocumentRequest(documentType="USER_GUIDE_OR_MANUAL", languages=["en-US"])
|
|
)
|
|
created_from_url = client.create_document_from_url(
|
|
CreateDocumentFromUrlRequest(
|
|
documentType="USER_GUIDE_OR_MANUAL",
|
|
documentUrl="https://example.test/guide.pdf",
|
|
languages=["en-US"],
|
|
)
|
|
)
|
|
fetched = client.get_document("DOC-1")
|
|
uploaded = client.upload_document(
|
|
"DOC-1",
|
|
file_name="guide.pdf",
|
|
content=b"%PDF-1.7",
|
|
content_type="application/pdf",
|
|
)
|
|
|
|
assert created.documentId == "DOC-1"
|
|
assert created_from_url.documentId == "DOC-2"
|
|
assert isinstance(fetched, DocumentResponse)
|
|
assert fetched.documentStatus == "ACCEPTED"
|
|
assert uploaded.documentStatus == "SUBMITTED"
|
|
upload_request = httpx_mock.get_requests()[3]
|
|
assert upload_request.headers["Content-Type"].startswith("multipart/form-data;")
|
|
assert b"filename=\"guide.pdf\"" in upload_request.content
|
|
assert b"%PDF-1.7" in upload_request.content |