From fa7c8caa104b61a171399150b85b4aed8fa1527e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Thu, 10 Jul 2025 05:16:25 +0200 Subject: [PATCH] Add tests for GameDetailView to validate expired campaign filtering --- twitch/tests/__init__.py | 0 twitch/tests/test_views.py | 83 ++++++++++++++++++++++++++++++++++++++ twitch/views.py | 26 +++++++----- 3 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 twitch/tests/__init__.py create mode 100644 twitch/tests/test_views.py diff --git a/twitch/tests/__init__.py b/twitch/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py new file mode 100644 index 0000000..ecdc5b6 --- /dev/null +++ b/twitch/tests/test_views.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from django.urls import reverse +from django.utils import timezone + +from twitch.models import DropCampaign, Game, Organization + +if TYPE_CHECKING: + from django.test import Client + + +@pytest.mark.django_db +class TestGameDetailView: + """Test cases for GameDetailView.""" + + def test_expired_campaigns_filtering(self, client: Client) -> None: + """Test that expired campaigns are correctly filtered.""" + # Create test data + game = Game.objects.create( + id="123", + slug="test-game", + display_name="Test Game", + ) + + organization = Organization.objects.create( + id="456", + name="Test Organization", + ) + + now = timezone.now() + + # Create an active campaign + active_campaign = DropCampaign.objects.create( + id="active-campaign", + name="Active Campaign", + game=game, + owner=organization, + start_at=now - timezone.timedelta(days=1), + end_at=now + timezone.timedelta(days=1), + status="ACTIVE", + ) + + # Create an expired campaign (end date in the past) + expired_by_date = DropCampaign.objects.create( + id="expired-by-date", + name="Expired By Date", + game=game, + owner=organization, + start_at=now - timezone.timedelta(days=3), + end_at=now - timezone.timedelta(days=1), + status="ACTIVE", # Still marked as active but date is expired + ) + + # Create an expired campaign (status is EXPIRED) + expired_by_status = DropCampaign.objects.create( + id="expired-by-status", + name="Expired By Status", + game=game, + owner=organization, + start_at=now - timezone.timedelta(days=3), + end_at=now + timezone.timedelta(days=1), + status="EXPIRED", # Explicitly expired + ) + + # Get the view context + url = reverse("twitch:game_detail", kwargs={"pk": game.id}) + response = client.get(url) + + # Check that active_campaigns only contains the active campaign + active_campaigns = response.context["active_campaigns"] + assert len(active_campaigns) == 1 + assert active_campaigns[0].id == active_campaign.id + + # Check that expired_campaigns contains only the expired campaigns + expired_campaigns = response.context["expired_campaigns"] + assert len(expired_campaigns) == 2 + expired_campaign_ids = [c.id for c in expired_campaigns] + assert expired_by_date.id in expired_campaign_ids + assert expired_by_status.id in expired_campaign_ids + assert active_campaign.id not in expired_campaign_ids diff --git a/twitch/views.py b/twitch/views.py index 591ba92..a625c5c 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -41,7 +41,7 @@ class DropCampaignListView(ListView): # Prefetch related objects to reduce queries return queryset.select_related("game", "owner").order_by("-start_at") - def get_context_data(self, **kwargs) -> dict[str, Any]: + def get_context_data(self, **kwargs: object) -> dict[str, Any]: """Add additional context data. Args: @@ -75,7 +75,7 @@ class DropCampaignDetailView(DetailView): template_name = "twitch/campaign_detail.html" context_object_name = "campaign" - def get_object(self, queryset=None): + def get_object(self, queryset: QuerySet[DropCampaign] | None = None) -> DropCampaign: """Get the campaign object with related data prefetched. Args: @@ -93,9 +93,9 @@ class DropCampaignDetailView(DetailView): # We don't need to prefetch time_based_drops here since we're fetching them separately in get_context_data # with proper ordering and prefetching of benefits - return super().get_object(queryset=queryset) + return super().get_object(queryset=queryset) # type: ignore[return-value] - def get_context_data(self, **kwargs) -> dict[str, Any]: + def get_context_data(self, **kwargs: object) -> dict[str, Any]: """Add additional context data. Args: @@ -129,7 +129,11 @@ class GameListView(ListView): context_object_name = "games" def get_queryset(self) -> QuerySet[Game]: - """Get queryset of games, annotated with campaign counts to avoid N+1 queries.""" + """Get queryset of games, annotated with campaign counts to avoid N+1 queries. + + Returns: + QuerySet[Game]: Queryset of games with annotations. + """ now = timezone.now() return ( super() @@ -149,7 +153,7 @@ class GameListView(ListView): .order_by("display_name") ) - def get_context_data(self, **kwargs) -> dict[str, Any]: + def get_context_data(self, **kwargs: object) -> dict[str, Any]: """Add additional context data with games grouped by organization. Args: @@ -225,14 +229,15 @@ class GameDetailView(DetailView): template_name = "twitch/game_detail.html" context_object_name = "game" - def get_context_data(self, **kwargs) -> dict[str, Any]: + def get_context_data(self, **kwargs: object) -> dict[str, Any]: """Add additional context data. Args: **kwargs: Additional arguments. Returns: - dict: Context data. + dict: Context data with active, upcoming, and expired campaigns. + Expired campaigns are filtered based on either end date or status. """ context = super().get_context_data(**kwargs) game = self.get_object() @@ -250,12 +255,13 @@ class GameDetailView(DetailView): upcoming_campaigns = [campaign for campaign in all_campaigns if campaign.start_at > now and campaign.status == "UPCOMING"] upcoming_campaigns.sort(key=lambda c: c.start_at) # Sort by start_at ascending - # No need to fetch expired_campaigns separately as we already have all_campaigns + # Filter for expired campaigns + expired_campaigns = [campaign for campaign in all_campaigns if campaign.end_at < now or campaign.status == "EXPIRED"] context.update({ "active_campaigns": active_campaigns, "upcoming_campaigns": upcoming_campaigns, - "expired_campaigns": all_campaigns, # We already have all campaigns sorted by -end_at + "expired_campaigns": expired_campaigns, # Only include expired campaigns "now": now, })