Implement emote gallery model method and refactor view to use it

This commit is contained in:
Joakim Hellsén 2026-04-12 04:53:08 +02:00
commit 1d524a2ca9
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
3 changed files with 239 additions and 25 deletions

View file

@ -3818,6 +3818,200 @@ class TestBadgeSetDetailView:
)
@pytest.mark.django_db
class TestEmoteGalleryView:
"""Tests for emote gallery model delegation and query safety."""
def test_emote_gallery_view_uses_model_helper(
self,
client: Client,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Emote gallery view should delegate data loading to the model layer."""
game: Game = Game.objects.create(
twitch_id="emote_gallery_delegate_game",
name="Emote Delegate Game",
display_name="Emote Delegate Game",
)
campaign: DropCampaign = DropCampaign.objects.create(
twitch_id="emote_gallery_delegate_campaign",
name="Emote Delegate Campaign",
game=game,
operation_names=["DropCampaignDetails"],
)
expected: list[dict[str, str | DropCampaign]] = [
{
"image_url": "https://example.com/emote.png",
"campaign": campaign,
},
]
calls: dict[str, int] = {"count": 0}
def _fake_emotes_for_gallery(
_cls: type[DropBenefit],
) -> list[dict[str, str | DropCampaign]]:
calls["count"] += 1
return expected
monkeypatch.setattr(
DropBenefit,
"emotes_for_gallery",
classmethod(_fake_emotes_for_gallery),
)
response: _MonkeyPatchedWSGIResponse = client.get(
reverse("twitch:emote_gallery"),
)
assert response.status_code == 200
context: ContextList | dict[str, Any] = response.context
if isinstance(context, list):
context = context[-1]
assert calls["count"] == 1
assert context["emotes"] == expected
def test_emotes_for_gallery_uses_prefetched_fields_without_extra_queries(
self,
) -> None:
"""Accessing template-used fields should not issue follow-up SELECT queries."""
now: datetime.datetime = timezone.now()
game: Game = Game.objects.create(
twitch_id="emote_gallery_fields_game",
name="Emote Fields Game",
display_name="Emote Fields Game",
)
campaign: DropCampaign = DropCampaign.objects.create(
twitch_id="emote_gallery_fields_campaign",
name="Emote Fields Campaign",
game=game,
operation_names=["DropCampaignDetails"],
start_at=now - timedelta(hours=1),
end_at=now + timedelta(hours=1),
)
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
twitch_id="emote_gallery_fields_drop",
name="Emote Fields Drop",
campaign=campaign,
)
benefit: DropBenefit = DropBenefit.objects.create(
twitch_id="emote_gallery_fields_benefit",
name="Emote Fields Benefit",
distribution_type="EMOTE",
image_asset_url="https://example.com/emote_fields.png",
)
drop.benefits.add(benefit)
emotes: list[dict[str, str | DropCampaign]] = DropBenefit.emotes_for_gallery()
assert len(emotes) == 1
with CaptureQueriesContext(connection) as capture:
for emote in emotes:
_ = emote["image_url"]
campaign_obj = emote["campaign"]
assert isinstance(campaign_obj, DropCampaign)
_ = campaign_obj.twitch_id
_ = campaign_obj.name
assert len(capture) == 0
def test_emotes_for_gallery_skips_emotes_without_campaign_link(self) -> None:
"""Gallery should only include EMOTE benefits reachable from a campaign drop."""
game: Game = Game.objects.create(
twitch_id="emote_gallery_skip_game",
name="Emote Skip Game",
display_name="Emote Skip Game",
)
campaign: DropCampaign = DropCampaign.objects.create(
twitch_id="emote_gallery_skip_campaign",
name="Emote Skip Campaign",
game=game,
operation_names=["DropCampaignDetails"],
)
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
twitch_id="emote_gallery_skip_drop",
name="Emote Skip Drop",
campaign=campaign,
)
included: DropBenefit = DropBenefit.objects.create(
twitch_id="emote_gallery_included_benefit",
name="Included Emote",
distribution_type="EMOTE",
image_asset_url="https://example.com/included-emote.png",
)
orphaned: DropBenefit = DropBenefit.objects.create(
twitch_id="emote_gallery_orphaned_benefit",
name="Orphaned Emote",
distribution_type="EMOTE",
image_asset_url="https://example.com/orphaned-emote.png",
)
drop.benefits.add(included)
emotes: list[dict[str, str | DropCampaign]] = DropBenefit.emotes_for_gallery()
image_urls: list[str] = [str(item["image_url"]) for item in emotes]
campaign_ids: list[str] = [
campaign_obj.twitch_id
for campaign_obj in (item["campaign"] for item in emotes)
if isinstance(campaign_obj, DropCampaign)
]
assert included.image_asset_url in image_urls
assert orphaned.image_asset_url not in image_urls
assert campaign.twitch_id in campaign_ids
def test_emote_gallery_view_renders_only_campaign_linked_emotes(
self,
client: Client,
) -> None:
"""Emote gallery page should not render EMOTE benefits without campaign-linked drops."""
game: Game = Game.objects.create(
twitch_id="emote_gallery_view_game",
name="Emote View Game",
display_name="Emote View Game",
)
campaign: DropCampaign = DropCampaign.objects.create(
twitch_id="emote_gallery_view_campaign",
name="Emote View Campaign",
game=game,
operation_names=["DropCampaignDetails"],
)
drop: TimeBasedDrop = TimeBasedDrop.objects.create(
twitch_id="emote_gallery_view_drop",
name="Emote View Drop",
campaign=campaign,
)
linked: DropBenefit = DropBenefit.objects.create(
twitch_id="emote_gallery_view_linked",
name="Linked Emote",
distribution_type="EMOTE",
image_asset_url="https://example.com/linked-view-emote.png",
)
orphaned: DropBenefit = DropBenefit.objects.create(
twitch_id="emote_gallery_view_orphaned",
name="Orphaned View Emote",
distribution_type="EMOTE",
image_asset_url="https://example.com/orphaned-view-emote.png",
)
drop.benefits.add(linked)
response: _MonkeyPatchedWSGIResponse = client.get(
reverse("twitch:emote_gallery"),
)
assert response.status_code == 200
html: str = response.content.decode("utf-8")
assert linked.image_asset_url in html
assert orphaned.image_asset_url not in html
assert reverse("twitch:campaign_detail", args=[campaign.twitch_id]) in html
@pytest.mark.django_db
class TestDropCampaignListView:
"""Tests for drop_campaign_list_view index usage and fat-model delegation."""