diff --git a/twitch/schemas.py b/twitch/schemas.py index baba55a..ac1afd0 100644 --- a/twitch/schemas.py +++ b/twitch/schemas.py @@ -88,7 +88,7 @@ class DropBenefitSchema(BaseModel): created_at: str | None = Field(default=None, alias="createdAt") entitlement_limit: int = Field(default=1, alias="entitlementLimit") is_ios_available: bool = Field(default=False, alias="isIosAvailable") - distribution_type: str = Field(alias="distributionType") + distribution_type: str | None = Field(default=None, alias="distributionType") # Optional in some API responses type_name: Literal["Benefit", "DropBenefit"] = Field(alias="__typename") # API response fields that should be ignored game: dict | None = None diff --git a/twitch/tests/test_schemas.py b/twitch/tests/test_schemas.py index b23b7c4..1a704a7 100644 --- a/twitch/tests/test_schemas.py +++ b/twitch/tests/test_schemas.py @@ -245,3 +245,131 @@ def test_graphql_response_with_errors() -> None: assert response.data.current_user.twitch_id == "17658559" assert response.data.current_user.drop_campaigns is not None assert len(response.data.current_user.drop_campaigns) == 1 + + +def test_drop_campaign_details_missing_distribution_type() -> None: + """Test that DropCampaignDetails operation validates when distributionType is missing. + + Some API responses (e.g., from DropCampaignDetails operation) don't include + the distributionType field in the benefit object. The schema should handle this + gracefully by making the field optional. + + This is based on a real-world validation error from file 7720168310842708008.json. + """ + payload = { + "data": { + "user": { + "id": "58162970", + "dropCampaign": { + "id": "1cf86f25-540f-11ef-90d0-0a58a9feac02", + "self": { + "isAccountConnected": False, + "__typename": "DropCampaignSelfEdge", + }, + "allow": { + "channels": [ + { + "id": "182565961", + "displayName": "WorldofWarships", + "name": "worldofwarships", + "__typename": "Channel", + }, + ], + "isEnabled": True, + "__typename": "DropCampaignACL", + }, + "accountLinkURL": "https://wargaming.net/personal/", + "description": "New Drops!", + "detailsURL": "https://worldofwarships.eu/news/general-news/stream-rewards-137/", + "endAt": "2024-08-21T21:59:59.996Z", + "eventBasedDrops": [], + "game": { + "id": "32502", + "slug": "world-of-warships", + "displayName": "World of Warships", + "__typename": "Game", + }, + "imageURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/51ec1931-6792-4b9f-850a-e6b9c454eefc.png", + "name": "Official Channel Weekly Drop", + "owner": { + "id": "6948a129-2c6d-4d88-9444-6b96918a19f8", + "name": "Wargaming", + "__typename": "Organization", + }, + "startAt": "2024-08-14T22:00:00Z", + "status": "ACTIVE", + "timeBasedDrops": [ + { + "id": "79a0a4db-5a4d-11ef-91b7-0a58a9feac02", + "requiredSubs": 0, + "benefitEdges": [ + { + "benefit": { + "id": "6948a129-2c6d-4d88-9444-6b96918a19f8_CUSTOM_ID_WOWS_TwitchDrops_1307_250ct", # noqa: E501 + "createdAt": "2024-08-06T16:03:15.89Z", + "entitlementLimit": 1, + "game": { + "id": "32502", + "name": "World of Warships", + "__typename": "Game", + }, + "imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/a049fb1a-ab63-40e5-9a43-bd6b94e52f36.png", + "isIosAvailable": False, + "name": "13.7 Update: 250 CT", + "ownerOrganization": { + "id": "6948a129-2c6d-4d88-9444-6b96918a19f8", + "name": "Wargaming", + "__typename": "Organization", + }, + "__typename": "DropBenefit", + }, + "entitlementLimit": 1, + "__typename": "DropBenefitEdge", + }, + ], + "endAt": "2024-08-21T21:59:59.996Z", + "name": "Week 2: 250 Community Tokens", + "preconditionDrops": None, + "requiredMinutesWatched": 60, + "startAt": "2024-08-14T22:00:00Z", + "__typename": "TimeBasedDrop", + }, + ], + "__typename": "DropCampaign", + }, + "__typename": "User", + }, + }, + "extensions": { + "durationMilliseconds": 75, + "operationName": "DropCampaignDetails", + "requestID": "01J5RDFXF7VMMDWVBZKNZ52ED6", + }, + } + + # This should not raise ValidationError despite missing distributionType + response = GraphQLResponse.model_validate(payload) + + # Verify the structure was parsed correctly + assert response.data.current_user is not None + assert response.data.current_user.twitch_id == "58162970" + + # Verify drop_campaigns was normalized from dropCampaign (singular) + assert response.data.current_user.drop_campaigns is not None + assert len(response.data.current_user.drop_campaigns) == 1 + + campaign = response.data.current_user.drop_campaigns[0] + assert campaign.name == "Official Channel Weekly Drop" + assert campaign.game.display_name == "World of Warships" + assert len(campaign.time_based_drops) == 1 + + # Verify time-based drops + first_drop = campaign.time_based_drops[0] + assert first_drop.name == "Week 2: 250 Community Tokens" + assert first_drop.required_minutes_watched == 60 + + # Verify benefits - distributionType should be None since it was missing + assert len(first_drop.benefit_edges) == 1 + benefit = first_drop.benefit_edges[0].benefit + assert benefit.name == "13.7 Update: 250 CT" + assert benefit.distribution_type is None # This field was missing in the API response