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, extract_resource_id 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" assert result.resource_id == "VIDEO-1" def test_extract_media_resource_id_handles_location_header() -> None: assert extract_resource_id("https://api.ebay.com/commerce/media/v1_beta/video/VIDEO-1") == "VIDEO-1" assert extract_resource_id("https://api.ebay.com/commerce/media/v1_beta/document/DOC-1/") == "DOC-1" assert extract_resource_id(None) is None 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 def test_media_wait_for_video_returns_live_payload(monkeypatch) -> None: client = MediaClient(build_transport()) states = iter( [ Video(videoId="VIDEO-1", status="PENDING_UPLOAD"), Video(videoId="VIDEO-1", status="PROCESSING"), Video(videoId="VIDEO-1", status="LIVE"), ] ) monkeypatch.setattr(client, "get_video", lambda _video_id: next(states)) monkeypatch.setattr("ebay_client.media.client.sleep", lambda _seconds: None) result = client.wait_for_video("VIDEO-1", poll_interval_seconds=0.0) assert result.status == "LIVE" def test_media_wait_for_document_raises_on_terminal_failure(monkeypatch) -> None: client = MediaClient(build_transport()) monkeypatch.setattr( client, "get_document", lambda _document_id: DocumentResponse(documentId="DOC-1", documentStatus="REJECTED"), ) try: client.wait_for_document("DOC-1", poll_interval_seconds=0.0) except ValueError as exc: assert "REJECTED" in str(exc) else: raise AssertionError("Expected wait_for_document to raise on terminal failure status")