Lower line-length to default and don't add from __future__ import annotations to everything
This commit is contained in:
parent
dcc4cecb8d
commit
1118c03c1b
46 changed files with 2338 additions and 1085 deletions
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
|
|
@ -142,7 +140,11 @@ class TestBackupCommand:
|
|||
assert output_dir.exists()
|
||||
assert len(list(output_dir.glob("test-*.sql.zst"))) == 1
|
||||
|
||||
def test_backup_uses_default_directory(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_backup_uses_default_directory(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that backup uses DATA_DIR/datasets by default."""
|
||||
_skip_if_pg_dump_missing()
|
||||
# Create test data so tables exist
|
||||
|
|
@ -285,7 +287,9 @@ class TestDatasetBackupViews:
|
|||
"""Test that dataset list view displays backup files."""
|
||||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"ttvdrops-20260210-120000.sql.zst" in response.content
|
||||
|
|
@ -300,7 +304,9 @@ class TestDatasetBackupViews:
|
|||
"""Test dataset list view with empty directory."""
|
||||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"No dataset backups found" in response.content
|
||||
|
|
@ -332,7 +338,9 @@ class TestDatasetBackupViews:
|
|||
os.utime(older_backup, (older_time, older_time))
|
||||
os.utime(newer_backup, (newer_time, newer_time))
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
|
||||
content = response.content.decode()
|
||||
newer_pos = content.find("20260210-140000")
|
||||
|
|
@ -352,7 +360,10 @@ class TestDatasetBackupViews:
|
|||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backup_download", args=["ttvdrops-20260210-120000.sql.zst"]),
|
||||
reverse(
|
||||
"twitch:dataset_backup_download",
|
||||
args=["ttvdrops-20260210-120000.sql.zst"],
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
|
@ -370,7 +381,9 @@ class TestDatasetBackupViews:
|
|||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
# Attempt path traversal
|
||||
response = client.get(reverse("twitch:dataset_backup_download", args=["../../../etc/passwd"]))
|
||||
response = client.get(
|
||||
reverse("twitch:dataset_backup_download", args=["../../../etc/passwd"]),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_dataset_download_rejects_invalid_extensions(
|
||||
|
|
@ -386,7 +399,9 @@ class TestDatasetBackupViews:
|
|||
invalid_file = datasets_dir / "malicious.exe"
|
||||
invalid_file.write_text("not a backup")
|
||||
|
||||
response = client.get(reverse("twitch:dataset_backup_download", args=["malicious.exe"]))
|
||||
response = client.get(
|
||||
reverse("twitch:dataset_backup_download", args=["malicious.exe"]),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_dataset_download_file_not_found(
|
||||
|
|
@ -398,7 +413,9 @@ class TestDatasetBackupViews:
|
|||
"""Test download returns 404 for non-existent file."""
|
||||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
response = client.get(reverse("twitch:dataset_backup_download", args=["nonexistent.sql.zst"]))
|
||||
response = client.get(
|
||||
reverse("twitch:dataset_backup_download", args=["nonexistent.sql.zst"]),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_dataset_list_view_shows_file_sizes(
|
||||
|
|
@ -411,7 +428,9 @@ class TestDatasetBackupViews:
|
|||
"""Test that file sizes are displayed in human-readable format."""
|
||||
monkeypatch.setattr(settings, "DATA_DIR", datasets_dir.parent)
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
# Should contain size information (bytes, KB, MB, or GB)
|
||||
|
|
@ -432,7 +451,9 @@ class TestDatasetBackupViews:
|
|||
(datasets_dir / "readme.txt").write_text("should be ignored")
|
||||
(datasets_dir / "old_backup.gz").write_bytes(b"should be ignored")
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
|
||||
content = response.content.decode()
|
||||
assert "backup.sql.zst" in content
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for chat badge views."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
|
@ -73,7 +71,9 @@ class TestBadgeSetDetailView:
|
|||
|
||||
def test_badge_set_detail_not_found(self, client: Client) -> None:
|
||||
"""Test 404 when badge set doesn't exist."""
|
||||
response = client.get(reverse("twitch:badge_set_detail", kwargs={"set_id": "nonexistent"}))
|
||||
response = client.get(
|
||||
reverse("twitch:badge_set_detail", kwargs={"set_id": "nonexistent"}),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_badge_set_detail_displays_badges(self, client: Client) -> None:
|
||||
|
|
@ -91,7 +91,9 @@ class TestBadgeSetDetailView:
|
|||
click_url="https://help.twitch.tv",
|
||||
)
|
||||
|
||||
response = client.get(reverse("twitch:badge_set_detail", kwargs={"set_id": "moderator"}))
|
||||
response = client.get(
|
||||
reverse("twitch:badge_set_detail", kwargs={"set_id": "moderator"}),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
|
||||
|
|
@ -113,7 +115,9 @@ class TestBadgeSetDetailView:
|
|||
description="VIP Badge",
|
||||
)
|
||||
|
||||
response = client.get(reverse("twitch:badge_set_detail", kwargs={"set_id": "vip"}))
|
||||
response = client.get(
|
||||
reverse("twitch:badge_set_detail", kwargs={"set_id": "vip"}),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
|
||||
|
|
@ -133,7 +137,9 @@ class TestBadgeSetDetailView:
|
|||
description="Test Badge",
|
||||
)
|
||||
|
||||
response = client.get(reverse("twitch:badge_set_detail", kwargs={"set_id": "test_set"}))
|
||||
response = client.get(
|
||||
reverse("twitch:badge_set_detail", kwargs={"set_id": "test_set"}),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import skipIf
|
||||
|
||||
from django.db import connection
|
||||
|
|
@ -9,7 +8,6 @@ from django.test import TestCase
|
|||
|
||||
from twitch.management.commands.better_import_drops import Command
|
||||
from twitch.management.commands.better_import_drops import detect_error_only_response
|
||||
from twitch.models import Channel
|
||||
from twitch.models import DropBenefit
|
||||
from twitch.models import DropCampaign
|
||||
from twitch.models import Game
|
||||
|
|
@ -17,25 +15,24 @@ from twitch.models import Organization
|
|||
from twitch.models import TimeBasedDrop
|
||||
from twitch.schemas import DropBenefitSchema
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from twitch.models import Channel
|
||||
|
||||
|
||||
class GetOrUpdateBenefitTests(TestCase):
|
||||
"""Tests for the _get_or_update_benefit method in better_import_drops.Command."""
|
||||
|
||||
def test_defaults_distribution_type_when_missing(self) -> None:
|
||||
"""Ensure importer sets distribution_type to empty string when absent."""
|
||||
command = Command()
|
||||
command.benefit_cache = {}
|
||||
|
||||
benefit_schema: DropBenefitSchema = DropBenefitSchema.model_validate(
|
||||
{
|
||||
"id": "benefit-missing-distribution-type",
|
||||
"name": "Test Benefit",
|
||||
"imageAssetURL": "https://example.com/benefit.png",
|
||||
"entitlementLimit": 1,
|
||||
"isIosAvailable": False,
|
||||
"__typename": "DropBenefit",
|
||||
},
|
||||
)
|
||||
command: Command = Command()
|
||||
benefit_schema: DropBenefitSchema = DropBenefitSchema.model_validate({
|
||||
"id": "benefit-missing-distribution-type",
|
||||
"name": "Test Benefit",
|
||||
"imageAssetURL": "https://example.com/benefit.png",
|
||||
"entitlementLimit": 1,
|
||||
"isIosAvailable": False,
|
||||
"__typename": "DropBenefit",
|
||||
})
|
||||
|
||||
benefit: DropBenefit = command._get_or_update_benefit(benefit_schema)
|
||||
|
||||
|
|
@ -64,7 +61,10 @@ class ExtractCampaignsTests(TestCase):
|
|||
"detailsURL": "http://example.com",
|
||||
"imageURL": "",
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": False, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": False,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "g1",
|
||||
"displayName": "Test Game",
|
||||
|
|
@ -82,9 +82,7 @@ class ExtractCampaignsTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "TestOp",
|
||||
},
|
||||
"extensions": {"operationName": "TestOp"},
|
||||
}
|
||||
|
||||
# Validate response
|
||||
|
|
@ -147,9 +145,7 @@ class ExtractCampaignsTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# Validate and process response
|
||||
|
|
@ -163,7 +159,9 @@ class ExtractCampaignsTests(TestCase):
|
|||
assert broken_dir is None
|
||||
|
||||
# Check that campaign was created with operation_name
|
||||
campaign: DropCampaign = DropCampaign.objects.get(twitch_id="inventory-campaign-1")
|
||||
campaign: DropCampaign = DropCampaign.objects.get(
|
||||
twitch_id="inventory-campaign-1",
|
||||
)
|
||||
assert campaign.name == "Test Inventory Campaign"
|
||||
assert campaign.operation_names == ["Inventory"]
|
||||
|
||||
|
|
@ -184,9 +182,7 @@ class ExtractCampaignsTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# Should validate successfully even with null campaigns
|
||||
|
|
@ -261,9 +257,7 @@ class ExtractCampaignsTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# Validate and process response
|
||||
|
|
@ -277,7 +271,9 @@ class ExtractCampaignsTests(TestCase):
|
|||
assert broken_dir is None
|
||||
|
||||
# Check that campaign was created and allow_is_enabled defaults to True
|
||||
campaign: DropCampaign = DropCampaign.objects.get(twitch_id="inventory-campaign-2")
|
||||
campaign: DropCampaign = DropCampaign.objects.get(
|
||||
twitch_id="inventory-campaign-2",
|
||||
)
|
||||
assert campaign.name == "Test ACL Campaign"
|
||||
assert campaign.allow_is_enabled is True # Should default to True
|
||||
|
||||
|
|
@ -304,10 +300,7 @@ class CampaignStructureDetectionTests(TestCase):
|
|||
"id": "123",
|
||||
"inventory": {
|
||||
"dropCampaignsInProgress": [
|
||||
{
|
||||
"id": "c1",
|
||||
"name": "Test Campaign",
|
||||
},
|
||||
{"id": "c1", "name": "Test Campaign"},
|
||||
],
|
||||
"__typename": "Inventory",
|
||||
},
|
||||
|
|
@ -349,12 +342,7 @@ class CampaignStructureDetectionTests(TestCase):
|
|||
"data": {
|
||||
"currentUser": {
|
||||
"id": "123",
|
||||
"dropCampaigns": [
|
||||
{
|
||||
"id": "c1",
|
||||
"name": "Test Campaign",
|
||||
},
|
||||
],
|
||||
"dropCampaigns": [{"id": "c1", "name": "Test Campaign"}],
|
||||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
|
|
@ -367,7 +355,10 @@ class CampaignStructureDetectionTests(TestCase):
|
|||
class OperationNameFilteringTests(TestCase):
|
||||
"""Tests for filtering campaigns by operation_name field."""
|
||||
|
||||
@skipIf(connection.vendor == "sqlite", reason="SQLite doesn't support JSON contains lookup")
|
||||
@skipIf(
|
||||
connection.vendor == "sqlite",
|
||||
reason="SQLite doesn't support JSON contains lookup",
|
||||
)
|
||||
def test_can_filter_campaigns_by_operation_name(self) -> None:
|
||||
"""Ensure campaigns can be filtered by operation_name to separate data sources."""
|
||||
command = Command()
|
||||
|
|
@ -388,7 +379,10 @@ class OperationNameFilteringTests(TestCase):
|
|||
"detailsURL": "https://example.com",
|
||||
"imageURL": "",
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": False, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": False,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "game-1",
|
||||
"displayName": "Game 1",
|
||||
|
|
@ -407,9 +401,7 @@ class OperationNameFilteringTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "ViewerDropsDashboard",
|
||||
},
|
||||
"extensions": {"operationName": "ViewerDropsDashboard"},
|
||||
}
|
||||
|
||||
# Import an Inventory campaign
|
||||
|
|
@ -429,7 +421,10 @@ class OperationNameFilteringTests(TestCase):
|
|||
"detailsURL": "https://example.com",
|
||||
"imageURL": "",
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": True, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": True,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "game-2",
|
||||
"displayName": "Game 2",
|
||||
|
|
@ -452,9 +447,7 @@ class OperationNameFilteringTests(TestCase):
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# Process both payloads
|
||||
|
|
@ -462,8 +455,12 @@ class OperationNameFilteringTests(TestCase):
|
|||
command.process_responses([inventory_payload], Path("inventory.json"), {})
|
||||
|
||||
# Verify we can filter by operation_names with JSON containment
|
||||
viewer_campaigns = DropCampaign.objects.filter(operation_names__contains=["ViewerDropsDashboard"])
|
||||
inventory_campaigns = DropCampaign.objects.filter(operation_names__contains=["Inventory"])
|
||||
viewer_campaigns = DropCampaign.objects.filter(
|
||||
operation_names__contains=["ViewerDropsDashboard"],
|
||||
)
|
||||
inventory_campaigns = DropCampaign.objects.filter(
|
||||
operation_names__contains=["Inventory"],
|
||||
)
|
||||
|
||||
assert len(viewer_campaigns) >= 1
|
||||
assert len(inventory_campaigns) >= 1
|
||||
|
|
@ -501,7 +498,10 @@ class GameImportTests(TestCase):
|
|||
"detailsURL": "https://example.com/details",
|
||||
"imageURL": "",
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": True, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": True,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "497057",
|
||||
"slug": "destiny-2",
|
||||
|
|
@ -558,12 +558,17 @@ class ExampleJsonImportTests(TestCase):
|
|||
assert success is True
|
||||
assert broken_dir is None
|
||||
|
||||
campaign: DropCampaign = DropCampaign.objects.get(twitch_id="3b965979-ecd2-11f0-876e-0a58a9feac02")
|
||||
campaign: DropCampaign = DropCampaign.objects.get(
|
||||
twitch_id="3b965979-ecd2-11f0-876e-0a58a9feac02",
|
||||
)
|
||||
|
||||
# Core campaign fields
|
||||
assert campaign.name == "Jan Drops Week 2"
|
||||
assert "Viewers will receive 50 Wandering Market Coins" in campaign.description
|
||||
assert campaign.details_url == "https://www.smite2.com/news/closed-alpha-twitch-drops/"
|
||||
assert (
|
||||
campaign.details_url
|
||||
== "https://www.smite2.com/news/closed-alpha-twitch-drops/"
|
||||
)
|
||||
assert campaign.account_link_url == "https://link.smite2.com/"
|
||||
|
||||
# The regression: ensure imageURL makes it into DropCampaign.image_url
|
||||
|
|
@ -584,17 +589,23 @@ class ExampleJsonImportTests(TestCase):
|
|||
assert game.display_name == "SMITE 2"
|
||||
assert game.slug == "smite-2"
|
||||
|
||||
org: Organization = Organization.objects.get(twitch_id="51a157a0-674a-4863-b120-7bb6ee2466a8")
|
||||
org: Organization = Organization.objects.get(
|
||||
twitch_id="51a157a0-674a-4863-b120-7bb6ee2466a8",
|
||||
)
|
||||
assert org.name == "Hi-Rez Studios"
|
||||
assert game.owners.filter(pk=org.pk).exists()
|
||||
|
||||
# Drops + benefits
|
||||
assert TimeBasedDrop.objects.filter(campaign=campaign).count() == 6
|
||||
first_drop: TimeBasedDrop = TimeBasedDrop.objects.get(twitch_id="933c8f91-ecd2-11f0-b3fd-0a58a9feac02")
|
||||
first_drop: TimeBasedDrop = TimeBasedDrop.objects.get(
|
||||
twitch_id="933c8f91-ecd2-11f0-b3fd-0a58a9feac02",
|
||||
)
|
||||
assert first_drop.name == "Market Coins Bundle 1"
|
||||
assert first_drop.required_minutes_watched == 120
|
||||
assert DropBenefit.objects.count() == 1
|
||||
benefit: DropBenefit = DropBenefit.objects.get(twitch_id="ccb3fb7f-e59b-11ef-aef0-0a58a9feac02")
|
||||
benefit: DropBenefit = DropBenefit.objects.get(
|
||||
twitch_id="ccb3fb7f-e59b-11ef-aef0-0a58a9feac02",
|
||||
)
|
||||
assert (
|
||||
benefit.image_asset_url
|
||||
== "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/903496ad-de97-41ff-ad97-12f099e20ea8.jpeg"
|
||||
|
|
@ -645,7 +656,10 @@ class ImporterRobustnessTests(TestCase):
|
|||
"detailsURL": "https://example.com/details",
|
||||
"imageURL": None,
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": False, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": False,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "g-null-image",
|
||||
"displayName": "Test Game",
|
||||
|
|
@ -694,12 +708,7 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
def test_detects_error_only_response_with_null_data(self) -> None:
|
||||
"""Ensure error-only response with null data field is detected."""
|
||||
parsed_json = {
|
||||
"errors": [
|
||||
{
|
||||
"message": "internal server error",
|
||||
"path": ["data"],
|
||||
},
|
||||
],
|
||||
"errors": [{"message": "internal server error", "path": ["data"]}],
|
||||
"data": None,
|
||||
}
|
||||
|
||||
|
|
@ -708,14 +717,7 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
|
||||
def test_detects_error_only_response_with_empty_data(self) -> None:
|
||||
"""Ensure error-only response with empty data dict is allowed through."""
|
||||
parsed_json = {
|
||||
"errors": [
|
||||
{
|
||||
"message": "unauthorized",
|
||||
},
|
||||
],
|
||||
"data": {},
|
||||
}
|
||||
parsed_json = {"errors": [{"message": "unauthorized"}], "data": {}}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
# Empty dict {} is considered "data exists" so this should pass
|
||||
|
|
@ -723,13 +725,7 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
|
||||
def test_detects_error_only_response_without_data_key(self) -> None:
|
||||
"""Ensure error-only response without data key is detected."""
|
||||
parsed_json = {
|
||||
"errors": [
|
||||
{
|
||||
"message": "missing data",
|
||||
},
|
||||
],
|
||||
}
|
||||
parsed_json = {"errors": [{"message": "missing data"}]}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
assert result == "error_only: missing data"
|
||||
|
|
@ -737,16 +733,8 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
def test_allows_response_with_both_errors_and_data(self) -> None:
|
||||
"""Ensure responses with both errors and valid data are not flagged."""
|
||||
parsed_json = {
|
||||
"errors": [
|
||||
{
|
||||
"message": "partial failure",
|
||||
},
|
||||
],
|
||||
"data": {
|
||||
"currentUser": {
|
||||
"dropCampaigns": [],
|
||||
},
|
||||
},
|
||||
"errors": [{"message": "partial failure"}],
|
||||
"data": {"currentUser": {"dropCampaigns": []}},
|
||||
}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
|
|
@ -754,28 +742,14 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
|
||||
def test_allows_response_with_no_errors(self) -> None:
|
||||
"""Ensure normal responses without errors are not flagged."""
|
||||
parsed_json = {
|
||||
"data": {
|
||||
"currentUser": {
|
||||
"dropCampaigns": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
parsed_json = {"data": {"currentUser": {"dropCampaigns": []}}}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
assert result is None
|
||||
|
||||
def test_detects_error_only_in_list_of_responses(self) -> None:
|
||||
"""Ensure error-only detection works with list of responses."""
|
||||
parsed_json = [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"message": "rate limit exceeded",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
parsed_json = [{"errors": [{"message": "rate limit exceeded"}]}]
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
assert result == "error_only: rate limit exceeded"
|
||||
|
|
@ -804,22 +778,14 @@ class ErrorOnlyResponseDetectionTests(TestCase):
|
|||
|
||||
def test_returns_none_for_empty_errors_list(self) -> None:
|
||||
"""Ensure empty errors list is not flagged as error-only."""
|
||||
parsed_json = {
|
||||
"errors": [],
|
||||
}
|
||||
parsed_json = {"errors": []}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
assert result is None
|
||||
|
||||
def test_handles_error_without_message_field(self) -> None:
|
||||
"""Ensure errors without message field use default text."""
|
||||
parsed_json = {
|
||||
"errors": [
|
||||
{
|
||||
"path": ["data"],
|
||||
},
|
||||
],
|
||||
}
|
||||
parsed_json = {"errors": [{"path": ["data"]}]}
|
||||
|
||||
result = detect_error_only_response(parsed_json)
|
||||
assert result == "error_only: unknown error"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for chat badge models and functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from django.db import IntegrityError
|
||||
from pydantic import ValidationError
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Test RSS feeds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractContextManager
|
||||
from datetime import timedelta
|
||||
|
|
@ -119,7 +117,10 @@ class RSSFeedTestCase(TestCase):
|
|||
|
||||
def test_organization_campaign_feed(self) -> None:
|
||||
"""Test organization-specific campaign feed returns 200."""
|
||||
url: str = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
url: str = reverse(
|
||||
"twitch:organization_campaign_feed",
|
||||
args=[self.org.twitch_id],
|
||||
)
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/rss+xml; charset=utf-8"
|
||||
|
|
@ -180,7 +181,10 @@ class RSSFeedTestCase(TestCase):
|
|||
)
|
||||
|
||||
# Get feed for first organization
|
||||
url: str = reverse("twitch:organization_campaign_feed", args=[self.org.twitch_id])
|
||||
url: str = reverse(
|
||||
"twitch:organization_campaign_feed",
|
||||
args=[self.org.twitch_id],
|
||||
)
|
||||
response: _MonkeyPatchedWSGIResponse = self.client.get(url)
|
||||
content: str = response.content.decode("utf-8")
|
||||
|
||||
|
|
@ -256,7 +260,10 @@ def _build_reward_campaign(game: Game, idx: int) -> RewardCampaign:
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_campaign_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_campaign_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Campaign feed should stay within a small, fixed query budget."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="test-org-queries",
|
||||
|
|
@ -274,7 +281,7 @@ def test_campaign_feed_queries_bounded(client: Client, django_assert_num_queries
|
|||
_build_campaign(game, i)
|
||||
|
||||
url: str = reverse("twitch:campaign_feed")
|
||||
# TODO(TheLovinator): 14 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: E501, TD003
|
||||
# TODO(TheLovinator): 14 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003
|
||||
with django_assert_num_queries(14, exact=False):
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -339,7 +346,10 @@ def test_campaign_feed_queries_do_not_scale_with_items(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_game_campaign_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_game_campaign_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Game campaign feed should not issue excess queries when rendering multiple campaigns."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="test-org-game-queries",
|
||||
|
|
@ -358,7 +368,7 @@ def test_game_campaign_feed_queries_bounded(client: Client, django_assert_num_qu
|
|||
|
||||
url: str = reverse("twitch:game_campaign_feed", args=[game.twitch_id])
|
||||
|
||||
# TODO(TheLovinator): 15 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: E501, TD003
|
||||
# TODO(TheLovinator): 15 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003
|
||||
with django_assert_num_queries(6, exact=False):
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -395,13 +405,13 @@ def test_game_campaign_feed_queries_do_not_scale_with_items(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_organization_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Organization RSS feed should stay within a modest query budget."""
|
||||
for i in range(5):
|
||||
Organization.objects.create(
|
||||
twitch_id=f"org-feed-{i}",
|
||||
name=f"Org Feed {i}",
|
||||
)
|
||||
Organization.objects.create(twitch_id=f"org-feed-{i}", name=f"Org Feed {i}")
|
||||
|
||||
url: str = reverse("twitch:organization_feed")
|
||||
with django_assert_num_queries(1, exact=True):
|
||||
|
|
@ -411,7 +421,10 @@ def test_organization_feed_queries_bounded(client: Client, django_assert_num_que
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_game_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_game_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Game RSS feed should stay within a modest query budget with multiple games."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="game-feed-org",
|
||||
|
|
@ -435,7 +448,10 @@ def test_game_feed_queries_bounded(client: Client, django_assert_num_queries: Qu
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organization_campaign_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_organization_campaign_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Organization campaign feed should not regress in query count."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org-campaign-feed",
|
||||
|
|
@ -453,7 +469,7 @@ def test_organization_campaign_feed_queries_bounded(client: Client, django_asser
|
|||
_build_campaign(game, i)
|
||||
|
||||
url: str = reverse("twitch:organization_campaign_feed", args=[org.twitch_id])
|
||||
# TODO(TheLovinator): 12 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: E501, TD003
|
||||
# TODO(TheLovinator): 12 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003
|
||||
with django_assert_num_queries(12, exact=False):
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -490,7 +506,10 @@ def test_organization_campaign_feed_queries_do_not_scale_with_items(
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reward_campaign_feed_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_reward_campaign_feed_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Reward campaign feed should stay within a modest query budget."""
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="reward-feed-org",
|
||||
|
|
@ -515,7 +534,10 @@ def test_reward_campaign_feed_queries_bounded(client: Client, django_assert_num_
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_docs_rss_queries_bounded(client: Client, django_assert_num_queries: QueryAsserter) -> None:
|
||||
def test_docs_rss_queries_bounded(
|
||||
client: Client,
|
||||
django_assert_num_queries: QueryAsserter,
|
||||
) -> None:
|
||||
"""Docs RSS page should stay within a reasonable query budget.
|
||||
|
||||
With limit=1 for documentation examples, we should have dramatically fewer queries
|
||||
|
|
@ -539,7 +561,7 @@ def test_docs_rss_queries_bounded(client: Client, django_assert_num_queries: Que
|
|||
|
||||
url: str = reverse("twitch:docs_rss")
|
||||
|
||||
# TODO(TheLovinator): 31 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: E501, TD003
|
||||
# TODO(TheLovinator): 31 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003
|
||||
with django_assert_num_queries(31, exact=False):
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -576,7 +598,11 @@ URL_NAMES: list[tuple[str, dict[str, str]]] = [
|
|||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(("url_name", "kwargs"), URL_NAMES)
|
||||
def test_rss_feeds_return_200(client: Client, url_name: str, kwargs: dict[str, str]) -> None:
|
||||
def test_rss_feeds_return_200(
|
||||
client: Client,
|
||||
url_name: str,
|
||||
kwargs: dict[str, str],
|
||||
) -> None:
|
||||
"""Test if feeds return HTTP 200.
|
||||
|
||||
Args:
|
||||
|
|
@ -626,9 +652,7 @@ def test_rss_feeds_return_200(client: Client, url_name: str, kwargs: dict[str, s
|
|||
display_name="TestChannel",
|
||||
)
|
||||
|
||||
badge_set: ChatBadgeSet = ChatBadgeSet.objects.create(
|
||||
set_id="test-set-123",
|
||||
)
|
||||
badge_set: ChatBadgeSet = ChatBadgeSet.objects.create(set_id="test-set-123")
|
||||
|
||||
_badge: ChatBadge = ChatBadge.objects.create(
|
||||
badge_set=badge_set,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.test import TestCase
|
||||
|
|
@ -30,7 +28,10 @@ class GameOwnerOrganizationTests(TestCase):
|
|||
"detailsURL": "https://help.twitch.tv/s/article/twitch-chat-badges-guide",
|
||||
"imageURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/495ebb6b-8134-4e51-b9d0-1f4a221b4f8d.png",
|
||||
"status": "ACTIVE",
|
||||
"self": {"isAccountConnected": True, "__typename": "DropCampaignSelfEdge"},
|
||||
"self": {
|
||||
"isAccountConnected": True,
|
||||
"__typename": "DropCampaignSelfEdge",
|
||||
},
|
||||
"game": {
|
||||
"id": "263490",
|
||||
"slug": "rust",
|
||||
|
|
@ -42,10 +43,18 @@ class GameOwnerOrganizationTests(TestCase):
|
|||
"__typename": "Organization",
|
||||
},
|
||||
},
|
||||
"owner": {"id": "other-org-id", "name": "Other Org", "__typename": "Organization"},
|
||||
"owner": {
|
||||
"id": "other-org-id",
|
||||
"name": "Other Org",
|
||||
"__typename": "Organization",
|
||||
},
|
||||
"timeBasedDrops": [],
|
||||
"eventBasedDrops": [],
|
||||
"allow": {"channels": None, "isEnabled": False, "__typename": "DropCampaignACL"},
|
||||
"allow": {
|
||||
"channels": None,
|
||||
"isEnabled": False,
|
||||
"__typename": "DropCampaignACL",
|
||||
},
|
||||
"__typename": "DropCampaign",
|
||||
},
|
||||
"__typename": "User",
|
||||
|
|
@ -65,7 +74,9 @@ class GameOwnerOrganizationTests(TestCase):
|
|||
|
||||
# Check game owners include Twitch Gaming and Other Org
|
||||
game: Game = Game.objects.get(twitch_id="263490")
|
||||
org1: Organization = Organization.objects.get(twitch_id="d32de13d-937e-4196-8198-1a7f875f295a")
|
||||
org1: Organization = Organization.objects.get(
|
||||
twitch_id="d32de13d-937e-4196-8198-1a7f875f295a",
|
||||
)
|
||||
org2: Organization = Organization.objects.get(twitch_id="other-org-id")
|
||||
owners = list(game.owners.all())
|
||||
assert org1 in owners
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for custom image template tags."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from django.template import Context
|
||||
from django.template import Template
|
||||
from django.utils.safestring import SafeString
|
||||
|
|
@ -19,11 +17,16 @@ class TestGetFormatUrl:
|
|||
|
||||
def test_jpg_to_webp(self) -> None:
|
||||
"""Test converting JPG to WebP."""
|
||||
assert get_format_url("/static/img/banner.jpg", "webp") == "/static/img/banner.webp"
|
||||
assert (
|
||||
get_format_url("/static/img/banner.jpg", "webp")
|
||||
== "/static/img/banner.webp"
|
||||
)
|
||||
|
||||
def test_jpeg_to_avif(self) -> None:
|
||||
"""Test converting JPEG to AVIF."""
|
||||
assert get_format_url("/static/img/photo.jpeg", "avif") == "/static/img/photo.avif"
|
||||
assert (
|
||||
get_format_url("/static/img/photo.jpeg", "avif") == "/static/img/photo.avif"
|
||||
)
|
||||
|
||||
def test_png_to_webp(self) -> None:
|
||||
"""Test converting PNG to WebP."""
|
||||
|
|
@ -31,7 +34,9 @@ class TestGetFormatUrl:
|
|||
|
||||
def test_uppercase_extension(self) -> None:
|
||||
"""Test converting uppercase extensions."""
|
||||
assert get_format_url("/static/img/photo.JPG", "webp") == "/static/img/photo.webp"
|
||||
assert (
|
||||
get_format_url("/static/img/photo.JPG", "webp") == "/static/img/photo.webp"
|
||||
)
|
||||
|
||||
def test_non_convertible_format(self) -> None:
|
||||
"""Test that non-convertible formats return unchanged."""
|
||||
|
|
@ -187,7 +192,9 @@ class TestPictureTag:
|
|||
|
||||
def test_twitch_cdn_url_simple_img(self) -> None:
|
||||
"""Test that Twitch CDN URLs return simple img tag without picture element."""
|
||||
result: SafeString = picture("https://static-cdn.jtvnw.net/ttv-boxart/1292861145.jpg")
|
||||
result: SafeString = picture(
|
||||
"https://static-cdn.jtvnw.net/ttv-boxart/1292861145.jpg",
|
||||
)
|
||||
|
||||
# Should NOT have picture element
|
||||
assert "<picture>" not in result
|
||||
|
|
@ -228,7 +235,9 @@ class TestPictureTag:
|
|||
|
||||
def test_twitch_cdn_url_with_png(self) -> None:
|
||||
"""Test Twitch CDN URL with PNG format."""
|
||||
result: SafeString = picture("https://static-cdn.jtvnw.net/badges/v1/1234567.png")
|
||||
result: SafeString = picture(
|
||||
"https://static-cdn.jtvnw.net/badges/v1/1234567.png",
|
||||
)
|
||||
|
||||
# Should NOT have picture element or source tags
|
||||
assert "<picture>" not in result
|
||||
|
|
@ -244,7 +253,9 @@ class TestPictureTagTemplate:
|
|||
|
||||
def test_picture_tag_in_template(self) -> None:
|
||||
"""Test that the picture tag works when called from a template."""
|
||||
template = Template('{% load image_tags %}{% picture src="/img/photo.jpg" alt="Test" %}')
|
||||
template = Template(
|
||||
'{% load image_tags %}{% picture src="/img/photo.jpg" alt="Test" %}',
|
||||
)
|
||||
context = Context({})
|
||||
result: SafeString = template.render(context)
|
||||
|
||||
|
|
@ -257,7 +268,9 @@ class TestPictureTagTemplate:
|
|||
|
||||
def test_picture_tag_with_context_variables(self) -> None:
|
||||
"""Test using context variables in the picture tag."""
|
||||
template = Template("{% load image_tags %}{% picture src=image_url alt=image_alt width=image_width %}")
|
||||
template = Template(
|
||||
"{% load image_tags %}{% picture src=image_url alt=image_alt width=image_width %}",
|
||||
)
|
||||
context = Context({
|
||||
"image_url": "/img/banner.png",
|
||||
"image_alt": "Banner image",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
"""Tests for Pydantic schemas used in the import process."""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from twitch.schemas import DropBenefitSchema
|
||||
from twitch.schemas import DropCampaignSchema
|
||||
from twitch.schemas import GameSchema
|
||||
from twitch.schemas import GraphQLResponse
|
||||
from twitch.schemas import TimeBasedDropSchema
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from twitch.schemas import DropBenefitSchema
|
||||
from twitch.schemas import DropCampaignSchema
|
||||
from twitch.schemas import TimeBasedDropSchema
|
||||
|
||||
|
||||
def test_inventory_operation_validation() -> None:
|
||||
|
|
@ -88,9 +90,7 @@ def test_inventory_operation_validation() -> None:
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# This should not raise ValidationError
|
||||
|
|
@ -121,16 +121,16 @@ def test_inventory_operation_validation() -> None:
|
|||
|
||||
def test_game_schema_normalizes_twitch_box_art_url() -> None:
|
||||
"""Ensure Twitch box art URLs are normalized for higher quality."""
|
||||
schema: GameSchema = GameSchema.model_validate(
|
||||
{
|
||||
"id": "65654",
|
||||
"displayName": "Test Game",
|
||||
"boxArtURL": "https://static-cdn.jtvnw.net/ttv-boxart/65654_IGDB-120x160.jpg",
|
||||
"__typename": "Game",
|
||||
},
|
||||
)
|
||||
schema: GameSchema = GameSchema.model_validate({
|
||||
"id": "65654",
|
||||
"displayName": "Test Game",
|
||||
"boxArtURL": "https://static-cdn.jtvnw.net/ttv-boxart/65654_IGDB-120x160.jpg",
|
||||
"__typename": "Game",
|
||||
})
|
||||
|
||||
assert schema.box_art_url == "https://static-cdn.jtvnw.net/ttv-boxart/65654_IGDB.jpg"
|
||||
assert (
|
||||
schema.box_art_url == "https://static-cdn.jtvnw.net/ttv-boxart/65654_IGDB.jpg"
|
||||
)
|
||||
|
||||
|
||||
def test_viewer_drops_dashboard_operation_still_works() -> None:
|
||||
|
|
@ -175,9 +175,7 @@ def test_viewer_drops_dashboard_operation_still_works() -> None:
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "ViewerDropsDashboard",
|
||||
},
|
||||
"extensions": {"operationName": "ViewerDropsDashboard"},
|
||||
}
|
||||
|
||||
# This should not raise ValidationError
|
||||
|
|
@ -201,11 +199,25 @@ def test_graphql_response_with_errors() -> None:
|
|||
"errors": [
|
||||
{
|
||||
"message": "service timeout",
|
||||
"path": ["currentUser", "inventory", "dropCampaignsInProgress", 7, "allow", "channels"],
|
||||
"path": [
|
||||
"currentUser",
|
||||
"inventory",
|
||||
"dropCampaignsInProgress",
|
||||
7,
|
||||
"allow",
|
||||
"channels",
|
||||
],
|
||||
},
|
||||
{
|
||||
"message": "service timeout",
|
||||
"path": ["currentUser", "inventory", "dropCampaignsInProgress", 10, "allow", "channels"],
|
||||
"path": [
|
||||
"currentUser",
|
||||
"inventory",
|
||||
"dropCampaignsInProgress",
|
||||
10,
|
||||
"allow",
|
||||
"channels",
|
||||
],
|
||||
},
|
||||
],
|
||||
"data": {
|
||||
|
|
@ -244,9 +256,7 @@ def test_graphql_response_with_errors() -> None:
|
|||
"__typename": "User",
|
||||
},
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "Inventory",
|
||||
},
|
||||
"extensions": {"operationName": "Inventory"},
|
||||
}
|
||||
|
||||
# This should not raise ValidationError even with errors field present
|
||||
|
|
@ -256,7 +266,14 @@ def test_graphql_response_with_errors() -> None:
|
|||
assert response.errors is not None
|
||||
assert len(response.errors) == 2
|
||||
assert response.errors[0].message == "service timeout"
|
||||
assert response.errors[0].path == ["currentUser", "inventory", "dropCampaignsInProgress", 7, "allow", "channels"]
|
||||
assert response.errors[0].path == [
|
||||
"currentUser",
|
||||
"inventory",
|
||||
"dropCampaignsInProgress",
|
||||
7,
|
||||
"allow",
|
||||
"channels",
|
||||
]
|
||||
|
||||
# Verify the data is still accessible and valid
|
||||
assert response.data.current_user is not None
|
||||
|
|
@ -323,7 +340,7 @@ def test_drop_campaign_details_missing_distribution_type() -> None:
|
|||
"benefitEdges": [
|
||||
{
|
||||
"benefit": {
|
||||
"id": "6948a129-2c6d-4d88-9444-6b96918a19f8_CUSTOM_ID_WOWS_TwitchDrops_1307_250ct", # noqa: E501
|
||||
"id": "6948a129-2c6d-4d88-9444-6b96918a19f8_CUSTOM_ID_WOWS_TwitchDrops_1307_250ct",
|
||||
"createdAt": "2024-08-06T16:03:15.89Z",
|
||||
"entitlementLimit": 1,
|
||||
"game": {
|
||||
|
|
@ -390,7 +407,9 @@ def test_drop_campaign_details_missing_distribution_type() -> None:
|
|||
assert len(first_drop.benefit_edges) == 1
|
||||
benefit: DropBenefitSchema = 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
|
||||
assert (
|
||||
benefit.distribution_type is None
|
||||
) # This field was missing in the API response
|
||||
|
||||
|
||||
def test_reward_campaigns_available_to_user() -> None:
|
||||
|
|
@ -454,9 +473,7 @@ def test_reward_campaigns_available_to_user() -> None:
|
|||
},
|
||||
],
|
||||
},
|
||||
"extensions": {
|
||||
"operationName": "ViewerDropsDashboard",
|
||||
},
|
||||
"extensions": {"operationName": "ViewerDropsDashboard"},
|
||||
}
|
||||
|
||||
# This should not raise ValidationError
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
|
@ -22,7 +20,6 @@ from twitch.models import DropCampaign
|
|||
from twitch.models import Game
|
||||
from twitch.models import Organization
|
||||
from twitch.models import TimeBasedDrop
|
||||
from twitch.views import Page
|
||||
from twitch.views import _build_breadcrumb_schema
|
||||
from twitch.views import _build_pagination_info
|
||||
from twitch.views import _build_seo_context
|
||||
|
|
@ -34,19 +31,26 @@ if TYPE_CHECKING:
|
|||
from django.test.client import _MonkeyPatchedWSGIResponse
|
||||
from django.test.utils import ContextList
|
||||
|
||||
from twitch.views import Page
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSearchView:
|
||||
"""Tests for the search_view function."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data(self) -> dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit]:
|
||||
def sample_data(
|
||||
self,
|
||||
) -> dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit]:
|
||||
"""Create sample data for testing.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the created sample data.
|
||||
"""
|
||||
org: Organization = Organization.objects.create(twitch_id="123", name="Test Organization")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="123",
|
||||
name="Test Organization",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="456",
|
||||
name="test_game",
|
||||
|
|
@ -78,7 +82,9 @@ class TestSearchView:
|
|||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_context(response: _MonkeyPatchedWSGIResponse) -> ContextList | dict[str, Any]:
|
||||
def _get_context(
|
||||
response: _MonkeyPatchedWSGIResponse,
|
||||
) -> ContextList | dict[str, Any]:
|
||||
"""Normalize Django test response context to a plain dict.
|
||||
|
||||
Args:
|
||||
|
|
@ -95,7 +101,10 @@ class TestSearchView:
|
|||
def test_empty_query(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test search with empty query returns no results."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/search/?q=")
|
||||
|
|
@ -108,7 +117,10 @@ class TestSearchView:
|
|||
def test_no_query_parameter(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test search with no query parameter returns no results."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/search/")
|
||||
|
|
@ -124,7 +136,10 @@ class TestSearchView:
|
|||
def test_short_query_istartswith(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
model_key: Literal["org", "game", "campaign", "drop", "benefit"],
|
||||
) -> None:
|
||||
"""Test short query (< 3 chars) uses istartswith for all models."""
|
||||
|
|
@ -151,7 +166,10 @@ class TestSearchView:
|
|||
def test_long_query_icontains(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
model_key: Literal["org", "game", "campaign", "drop", "benefit"],
|
||||
) -> None:
|
||||
"""Test long query (>= 3 chars) uses icontains for all models."""
|
||||
|
|
@ -174,7 +192,10 @@ class TestSearchView:
|
|||
def test_campaign_description_search(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test that campaign description is searchable."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/search/?q=campaign")
|
||||
|
|
@ -186,7 +207,10 @@ class TestSearchView:
|
|||
def test_game_display_name_search(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test that game display_name is searchable."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/search/?q=Game")
|
||||
|
|
@ -198,7 +222,10 @@ class TestSearchView:
|
|||
def test_query_no_matches(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test search with query that has no matches."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/search/?q=xyz")
|
||||
|
|
@ -211,7 +238,10 @@ class TestSearchView:
|
|||
def test_context_contains_query(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
) -> None:
|
||||
"""Test that context contains the search query."""
|
||||
query = "Test"
|
||||
|
|
@ -222,15 +252,15 @@ class TestSearchView:
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
("model_key", "related_field"),
|
||||
[
|
||||
("campaigns", "game"),
|
||||
("drops", "campaign"),
|
||||
],
|
||||
[("campaigns", "game"), ("drops", "campaign")],
|
||||
)
|
||||
def test_select_related_optimization(
|
||||
self,
|
||||
client: Client,
|
||||
sample_data: dict[str, Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit],
|
||||
sample_data: dict[
|
||||
str,
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit,
|
||||
],
|
||||
model_key: str,
|
||||
related_field: str,
|
||||
) -> None:
|
||||
|
|
@ -238,11 +268,15 @@ class TestSearchView:
|
|||
response: _MonkeyPatchedWSGIResponse = client.get("/search/?q=Test")
|
||||
context: ContextList | dict[str, Any] = self._get_context(response)
|
||||
|
||||
results: list[Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit] = context["results"][model_key]
|
||||
results: list[
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit
|
||||
] = context["results"][model_key]
|
||||
assert len(results) > 0
|
||||
|
||||
# Verify the related object is accessible without additional query
|
||||
first_result: Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit = results[0]
|
||||
first_result: (
|
||||
Organization | Game | DropCampaign | TimeBasedDrop | DropBenefit
|
||||
) = results[0]
|
||||
assert hasattr(first_result, related_field)
|
||||
|
||||
|
||||
|
|
@ -251,13 +285,18 @@ class TestChannelListView:
|
|||
"""Tests for the ChannelListView."""
|
||||
|
||||
@pytest.fixture
|
||||
def channel_with_campaigns(self) -> dict[str, Channel | Game | Organization | list[DropCampaign]]:
|
||||
def channel_with_campaigns(
|
||||
self,
|
||||
) -> dict[str, Channel | Game | Organization | list[DropCampaign]]:
|
||||
"""Create a channel with multiple campaigns for testing.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the created channel and campaigns.
|
||||
"""
|
||||
org: Organization = Organization.objects.create(twitch_id="org1", name="Test Org")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org1",
|
||||
name="Test Org",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
|
|
@ -284,12 +323,7 @@ class TestChannelListView:
|
|||
campaign.allow_channels.add(channel)
|
||||
campaigns.append(campaign)
|
||||
|
||||
return {
|
||||
"channel": channel,
|
||||
"campaigns": campaigns,
|
||||
"game": game,
|
||||
"org": org,
|
||||
}
|
||||
return {"channel": channel, "campaigns": campaigns, "game": game, "org": org}
|
||||
|
||||
def test_channel_list_loads(self, client: Client) -> None:
|
||||
"""Test that channel list view loads successfully."""
|
||||
|
|
@ -299,7 +333,10 @@ class TestChannelListView:
|
|||
def test_campaign_count_annotation(
|
||||
self,
|
||||
client: Client,
|
||||
channel_with_campaigns: dict[str, Channel | Game | Organization | list[DropCampaign]],
|
||||
channel_with_campaigns: dict[
|
||||
str,
|
||||
Channel | Game | Organization | list[DropCampaign],
|
||||
],
|
||||
) -> None:
|
||||
"""Test that campaign_count is correctly annotated for channels."""
|
||||
channel: Channel = channel_with_campaigns["channel"] # type: ignore[assignment]
|
||||
|
|
@ -313,13 +350,18 @@ class TestChannelListView:
|
|||
channels: list[Channel] = context["channels"]
|
||||
|
||||
# Find our test channel in the results
|
||||
test_channel: Channel | None = next((ch for ch in channels if ch.twitch_id == channel.twitch_id), None)
|
||||
test_channel: Channel | None = next(
|
||||
(ch for ch in channels if ch.twitch_id == channel.twitch_id),
|
||||
None,
|
||||
)
|
||||
|
||||
assert test_channel is not None
|
||||
assert hasattr(test_channel, "campaign_count")
|
||||
|
||||
campaign_count: int | None = getattr(test_channel, "campaign_count", None)
|
||||
assert campaign_count == len(campaigns), f"Expected campaign_count to be {len(campaigns)}, got {campaign_count}"
|
||||
assert campaign_count == len(campaigns), (
|
||||
f"Expected campaign_count to be {len(campaigns)}, got {campaign_count}"
|
||||
)
|
||||
|
||||
def test_campaign_count_zero_for_channel_without_campaigns(
|
||||
self,
|
||||
|
|
@ -339,7 +381,10 @@ class TestChannelListView:
|
|||
context = context[-1]
|
||||
|
||||
channels: list[Channel] = context["channels"]
|
||||
test_channel: Channel | None = next((ch for ch in channels if ch.twitch_id == channel.twitch_id), None)
|
||||
test_channel: Channel | None = next(
|
||||
(ch for ch in channels if ch.twitch_id == channel.twitch_id),
|
||||
None,
|
||||
)
|
||||
|
||||
assert test_channel is not None
|
||||
assert hasattr(test_channel, "campaign_count")
|
||||
|
|
@ -350,7 +395,10 @@ class TestChannelListView:
|
|||
def test_channels_ordered_by_campaign_count(
|
||||
self,
|
||||
client: Client,
|
||||
channel_with_campaigns: dict[str, Channel | Game | Organization | list[DropCampaign]],
|
||||
channel_with_campaigns: dict[
|
||||
str,
|
||||
Channel | Game | Organization | list[DropCampaign],
|
||||
],
|
||||
) -> None:
|
||||
"""Test that channels are ordered by campaign_count descending."""
|
||||
game: Game = channel_with_campaigns["game"] # type: ignore[assignment]
|
||||
|
|
@ -380,17 +428,28 @@ class TestChannelListView:
|
|||
channels: list[Channel] = list(context["channels"])
|
||||
|
||||
# The channel with 10 campaigns should come before the one with 5
|
||||
channel2_index: int | None = next((i for i, ch in enumerate(channels) if ch.twitch_id == "channel2"), None)
|
||||
channel1_index: int | None = next((i for i, ch in enumerate(channels) if ch.twitch_id == "channel1"), None)
|
||||
channel2_index: int | None = next(
|
||||
(i for i, ch in enumerate(channels) if ch.twitch_id == "channel2"),
|
||||
None,
|
||||
)
|
||||
channel1_index: int | None = next(
|
||||
(i for i, ch in enumerate(channels) if ch.twitch_id == "channel1"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert channel2_index is not None
|
||||
assert channel1_index is not None
|
||||
assert channel2_index < channel1_index, "Channel with more campaigns should appear first"
|
||||
assert channel2_index < channel1_index, (
|
||||
"Channel with more campaigns should appear first"
|
||||
)
|
||||
|
||||
def test_channel_search_filters_correctly(
|
||||
self,
|
||||
client: Client,
|
||||
channel_with_campaigns: dict[str, Channel | Game | Organization | list[DropCampaign]],
|
||||
channel_with_campaigns: dict[
|
||||
str,
|
||||
Channel | Game | Organization | list[DropCampaign],
|
||||
],
|
||||
) -> None:
|
||||
"""Test that search parameter filters channels correctly."""
|
||||
channel: Channel = channel_with_campaigns["channel"] # type: ignore[assignment]
|
||||
|
|
@ -402,7 +461,9 @@ class TestChannelListView:
|
|||
display_name="OtherChannel",
|
||||
)
|
||||
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(f"/channels/?search={channel.name}")
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
f"/channels/?search={channel.name}",
|
||||
)
|
||||
context: ContextList | dict[str, Any] = response.context # type: ignore[assignment]
|
||||
if isinstance(context, list):
|
||||
context = context[-1]
|
||||
|
|
@ -421,12 +482,25 @@ class TestChannelListView:
|
|||
assert "active_campaigns" in response.context
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dashboard_dedupes_campaigns_for_multi_owner_game(self, client: Client) -> None:
|
||||
def test_dashboard_dedupes_campaigns_for_multi_owner_game(
|
||||
self,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Dashboard should not render duplicate campaign cards when a game has multiple owners."""
|
||||
now = timezone.now()
|
||||
org1: Organization = Organization.objects.create(twitch_id="org_a", name="Org A")
|
||||
org2: Organization = Organization.objects.create(twitch_id="org_b", name="Org B")
|
||||
game: Game = Game.objects.create(twitch_id="game_multi_owner", name="game", display_name="Multi Owner")
|
||||
org1: Organization = Organization.objects.create(
|
||||
twitch_id="org_a",
|
||||
name="Org A",
|
||||
)
|
||||
org2: Organization = Organization.objects.create(
|
||||
twitch_id="org_b",
|
||||
name="Org B",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game_multi_owner",
|
||||
name="game",
|
||||
display_name="Multi Owner",
|
||||
)
|
||||
game.owners.add(org1, org2)
|
||||
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
|
|
@ -463,14 +537,20 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_view(self, client: Client) -> None:
|
||||
"""Test campaign list view returns 200 and has campaigns in context."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:campaign_list"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:campaign_list"),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "campaigns" in response.context
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_pagination(self, client: Client) -> None:
|
||||
"""Test pagination works correctly with 100 items per page."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create 150 campaigns to test pagination
|
||||
|
|
@ -488,7 +568,9 @@ class TestChannelListView:
|
|||
DropCampaign.objects.bulk_create(campaigns)
|
||||
|
||||
# Test first page
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:campaign_list"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:campaign_list"),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "is_paginated" in response.context
|
||||
assert response.context["is_paginated"] is True
|
||||
|
|
@ -508,7 +590,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_status_filter_active(self, client: Client) -> None:
|
||||
"""Test filtering for active campaigns only."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create active campaign
|
||||
|
|
@ -553,7 +639,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_status_filter_upcoming(self, client: Client) -> None:
|
||||
"""Test filtering for upcoming campaigns only."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create active campaign
|
||||
|
|
@ -598,7 +688,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_status_filter_expired(self, client: Client) -> None:
|
||||
"""Test filtering for expired campaigns only."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create active campaign
|
||||
|
|
@ -643,8 +737,16 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_game_filter(self, client: Client) -> None:
|
||||
"""Test filtering campaigns by game."""
|
||||
game1: Game = Game.objects.create(twitch_id="g1", name="Game 1", display_name="Game 1")
|
||||
game2: Game = Game.objects.create(twitch_id="g2", name="Game 2", display_name="Game 2")
|
||||
game1: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game 1",
|
||||
display_name="Game 1",
|
||||
)
|
||||
game2: Game = Game.objects.create(
|
||||
twitch_id="g2",
|
||||
name="Game 2",
|
||||
display_name="Game 2",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create campaigns for game 1
|
||||
|
|
@ -692,9 +794,16 @@ class TestChannelListView:
|
|||
assert campaigns[0].game.twitch_id == "g2"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_drop_campaign_list_pagination_preserves_filters(self, client: Client) -> None:
|
||||
def test_drop_campaign_list_pagination_preserves_filters(
|
||||
self,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Test that pagination links preserve game and status filters."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
now: datetime.datetime = timezone.now()
|
||||
|
||||
# Create 150 active campaigns for game g1
|
||||
|
|
@ -726,7 +835,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_drop_campaign_detail_view(self, client: Client, db: object) -> None:
|
||||
"""Test campaign detail view returns 200 and has campaign in context."""
|
||||
game: Game = Game.objects.create(twitch_id="g1", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g1",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="c1",
|
||||
name="Campaign",
|
||||
|
|
@ -744,7 +857,11 @@ class TestChannelListView:
|
|||
client: Client,
|
||||
) -> None:
|
||||
"""Test campaign detail view includes badge benefit description from ChatBadge."""
|
||||
game: Game = Game.objects.create(twitch_id="g-badge", name="Game", display_name="Game")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g-badge",
|
||||
name="Game",
|
||||
display_name="Game",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="c-badge",
|
||||
name="Campaign",
|
||||
|
|
@ -803,7 +920,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_game_detail_view(self, client: Client, db: object) -> None:
|
||||
"""Test game detail view returns 200 and has game in context."""
|
||||
game: Game = Game.objects.create(twitch_id="g2", name="Game2", display_name="Game2")
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="g2",
|
||||
name="Game2",
|
||||
display_name="Game2",
|
||||
)
|
||||
url: str = reverse("twitch:game_detail", args=[game.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
|
@ -828,7 +949,11 @@ class TestChannelListView:
|
|||
@pytest.mark.django_db
|
||||
def test_channel_detail_view(self, client: Client, db: object) -> None:
|
||||
"""Test channel detail view returns 200 and has channel in context."""
|
||||
channel: Channel = Channel.objects.create(twitch_id="ch1", name="Channel1", display_name="Channel1")
|
||||
channel: Channel = Channel.objects.create(
|
||||
twitch_id="ch1",
|
||||
name="Channel1",
|
||||
display_name="Channel1",
|
||||
)
|
||||
url: str = reverse("twitch:channel_detail", args=[channel.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
assert response.status_code == 200
|
||||
|
|
@ -858,7 +983,7 @@ class TestSEOHelperFunctions:
|
|||
|
||||
def test_truncate_description_long_text(self) -> None:
|
||||
"""Test that long text is truncated at word boundary."""
|
||||
text = "This is a very long description that exceeds the maximum length and should be truncated at a word boundary to avoid cutting off in the middle of a word" # noqa: E501
|
||||
text = "This is a very long description that exceeds the maximum length and should be truncated at a word boundary to avoid cutting off in the middle of a word"
|
||||
result: str = _truncate_description(text, max_length=50)
|
||||
assert len(result) <= 53 # Allow some flexibility
|
||||
assert not result.endswith(" ")
|
||||
|
|
@ -890,7 +1015,9 @@ class TestSEOHelperFunctions:
|
|||
def test_build_seo_context_with_all_parameters(self) -> None:
|
||||
"""Test _build_seo_context with all parameters."""
|
||||
now: datetime.datetime = timezone.now()
|
||||
breadcrumb: list[dict[str, int | str]] = [{"position": 1, "name": "Home", "url": "/"}]
|
||||
breadcrumb: list[dict[str, int | str]] = [
|
||||
{"position": 1, "name": "Home", "url": "/"},
|
||||
]
|
||||
|
||||
context: dict[str, Any] = _build_seo_context(
|
||||
page_title="Test",
|
||||
|
|
@ -938,7 +1065,11 @@ class TestSEOHelperFunctions:
|
|||
paginator: Paginator[int] = Paginator(items, 10)
|
||||
page: Page[int] = paginator.get_page(1)
|
||||
|
||||
info: list[dict[str, str]] | None = _build_pagination_info(request, page, "/campaigns/")
|
||||
info: list[dict[str, str]] | None = _build_pagination_info(
|
||||
request,
|
||||
page,
|
||||
"/campaigns/",
|
||||
)
|
||||
|
||||
assert info is not None
|
||||
assert len(info) == 1
|
||||
|
|
@ -954,7 +1085,11 @@ class TestSEOHelperFunctions:
|
|||
paginator: Paginator[int] = Paginator(items, 10)
|
||||
page: Page[int] = paginator.get_page(2)
|
||||
|
||||
info: list[dict[str, str]] | None = _build_pagination_info(request, page, "/campaigns/")
|
||||
info: list[dict[str, str]] | None = _build_pagination_info(
|
||||
request,
|
||||
page,
|
||||
"/campaigns/",
|
||||
)
|
||||
|
||||
assert info is not None
|
||||
assert len(info) == 2
|
||||
|
|
@ -975,7 +1110,10 @@ class TestSEOMetaTags:
|
|||
Returns:
|
||||
dict[str, Any]: A dictionary containing the created organization, game, and campaign.
|
||||
"""
|
||||
org: Organization = Organization.objects.create(twitch_id="org1", name="Test Org")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org1",
|
||||
name="Test Org",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
|
|
@ -995,7 +1133,9 @@ class TestSEOMetaTags:
|
|||
|
||||
def test_campaign_list_view_has_seo_context(self, client: Client) -> None:
|
||||
"""Test campaign list view has SEO context variables."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:campaign_list"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:campaign_list"),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "page_title" in response.context
|
||||
assert "page_description" in response.context
|
||||
|
|
@ -1050,7 +1190,10 @@ class TestSEOMetaTags:
|
|||
|
||||
def test_organization_detail_view_has_breadcrumb(self, client: Client) -> None:
|
||||
"""Test organization detail view has breadcrumb."""
|
||||
org: Organization = Organization.objects.create(twitch_id="org1", name="Test Org")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org1",
|
||||
name="Test Org",
|
||||
)
|
||||
url: str = reverse("twitch:organization_detail", args=[org.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -1059,7 +1202,11 @@ class TestSEOMetaTags:
|
|||
|
||||
def test_channel_detail_view_has_breadcrumb(self, client: Client) -> None:
|
||||
"""Test channel detail view has breadcrumb."""
|
||||
channel: Channel = Channel.objects.create(twitch_id="ch1", name="ch1", display_name="Channel 1")
|
||||
channel: Channel = Channel.objects.create(
|
||||
twitch_id="ch1",
|
||||
name="ch1",
|
||||
display_name="Channel 1",
|
||||
)
|
||||
url: str = reverse("twitch:channel_detail", args=[channel.twitch_id])
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(url)
|
||||
|
||||
|
|
@ -1068,10 +1215,11 @@ class TestSEOMetaTags:
|
|||
|
||||
def test_noindex_pages_have_robots_directive(self, client: Client) -> None:
|
||||
"""Test that pages with noindex have proper robots directive."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(reverse("twitch:dataset_backups"))
|
||||
response: _MonkeyPatchedWSGIResponse = client.get(
|
||||
reverse("twitch:dataset_backups"),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "robots_directive" in response.context
|
||||
assert "noindex" in response.context["robots_directive"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -1085,14 +1233,21 @@ class TestSitemapView:
|
|||
Returns:
|
||||
dict[str, Any]: A dictionary containing the created organization, game, channel, campaign, and badge set.
|
||||
"""
|
||||
org: Organization = Organization.objects.create(twitch_id="org1", name="Test Org")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org1",
|
||||
name="Test Org",
|
||||
)
|
||||
game: Game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
display_name="Test Game",
|
||||
)
|
||||
game.owners.add(org)
|
||||
channel: Channel = Channel.objects.create(twitch_id="ch1", name="ch1", display_name="Channel 1")
|
||||
channel: Channel = Channel.objects.create(
|
||||
twitch_id="ch1",
|
||||
name="ch1",
|
||||
display_name="Channel 1",
|
||||
)
|
||||
campaign: DropCampaign = DropCampaign.objects.create(
|
||||
twitch_id="camp1",
|
||||
name="Test Campaign",
|
||||
|
|
@ -1109,31 +1264,50 @@ class TestSitemapView:
|
|||
"badge": badge,
|
||||
}
|
||||
|
||||
def test_sitemap_view_returns_xml(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_view_returns_xml(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap view returns XML content."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/xml"
|
||||
|
||||
def test_sitemap_contains_xml_declaration(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_contains_xml_declaration(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap contains proper XML declaration."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content = response.content.decode()
|
||||
assert content.startswith('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
|
||||
def test_sitemap_contains_urlset(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_contains_urlset(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap contains urlset element."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "<urlset" in content
|
||||
assert "</urlset>" in content
|
||||
|
||||
def test_sitemap_contains_static_pages(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_contains_static_pages(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes static pages."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
# Check for some static pages
|
||||
assert "<loc>http://testserver/</loc>" in content or "<loc>http://localhost:8000/</loc>" in content
|
||||
assert (
|
||||
"<loc>http://testserver/</loc>" in content
|
||||
or "<loc>http://localhost:8000/</loc>" in content
|
||||
)
|
||||
assert "/campaigns/" in content
|
||||
assert "/games/" in content
|
||||
|
||||
|
|
@ -1192,21 +1366,33 @@ class TestSitemapView:
|
|||
content: str = response.content.decode()
|
||||
assert f"/badges/{badge.set_id}/" in content # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def test_sitemap_includes_priority(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_includes_priority(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes priority values."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "<priority>" in content
|
||||
assert "</priority>" in content
|
||||
|
||||
def test_sitemap_includes_changefreq(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_includes_changefreq(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes changefreq values."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
assert "<changefreq>" in content
|
||||
assert "</changefreq>" in content
|
||||
|
||||
def test_sitemap_includes_lastmod(self, client: Client, sample_entities: dict[str, Any]) -> None:
|
||||
def test_sitemap_includes_lastmod(
|
||||
self,
|
||||
client: Client,
|
||||
sample_entities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test sitemap includes lastmod for detail pages."""
|
||||
response: _MonkeyPatchedWSGIResponse = client.get("/sitemap.xml")
|
||||
content: str = response.content.decode()
|
||||
|
|
@ -1275,7 +1461,10 @@ class TestSEOPaginationLinks:
|
|||
def test_campaign_list_first_page_has_next(self, client: Client) -> None:
|
||||
"""Test campaign list first page has next link."""
|
||||
# Create a game and multiple campaigns to trigger pagination
|
||||
org: Organization = Organization.objects.create(twitch_id="org1", name="Test Org")
|
||||
org: Organization = Organization.objects.create(
|
||||
twitch_id="org1",
|
||||
name="Test Org",
|
||||
)
|
||||
game = Game.objects.create(
|
||||
twitch_id="game1",
|
||||
name="test_game",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue