diff --git a/core/tests/test_sitemaps.py b/core/tests/test_sitemaps.py
index 4486791..c833d26 100644
--- a/core/tests/test_sitemaps.py
+++ b/core/tests/test_sitemaps.py
@@ -23,7 +23,7 @@ def test_sitemap_static_contains_expected_links(
youtube apps as well as some misc static files like /robots.txt.
"""
# Ensure deterministic BASE_URL
- settings.BASE_URL = "https://ttvdrops.lovinator.space"
+ settings.BASE_URL = "https://ttvdrops.lovinator.space" # pyright: ignore[reportAttributeAccessIssue]
response = client.get(reverse("sitemap-static"))
assert response.status_code == 200
@@ -31,7 +31,7 @@ def test_sitemap_static_contains_expected_links(
locs = _extract_locs(response.content)
- base = settings.BASE_URL.rstrip("/")
+ base = settings.BASE_URL.rstrip("/") # pyright: ignore[reportAttributeAccessIssue]
expected_paths = [
reverse("core:dashboard"),
diff --git a/core/views.py b/core/views.py
index 464ded6..2c6e8b1 100644
--- a/core/views.py
+++ b/core/views.py
@@ -22,8 +22,8 @@ from django.http import Http404
from django.http import HttpRequest
from django.http import HttpResponse
from django.shortcuts import render
-from django.shortcuts import reverse
from django.template.defaultfilters import filesizeformat
+from django.urls import reverse
from django.utils import timezone
from kick.models import KickChannel
diff --git a/pyproject.toml b/pyproject.toml
index 28e688f..0b8e299 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,14 +5,20 @@ description = "Get notified when a new drop is available on Twitch."
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
+ "celery[redis]",
"colorama",
"dateparser",
"django-auto-prefetch",
+ "django-celery-beat",
+ "django-celery-results",
"django-debug-toolbar",
"django-silk",
"django",
+ "flower",
"gunicorn",
+ "hiredis",
"httpx",
+ "index-now-for-python",
"json-repair",
"pillow",
"platformdirs",
@@ -20,17 +26,11 @@ dependencies = [
"pydantic",
"pygments",
"python-dotenv",
+ "redis",
"sentry-sdk",
"setproctitle",
- "tqdm",
- "index-now-for-python",
"sitemap-parser",
- "celery[redis]",
- "django-celery-results",
- "django-celery-beat",
- "flower",
- "hiredis",
- "redis",
+ "tqdm",
]
@@ -41,9 +41,9 @@ dev = [
"hypothesis[django]",
"pytest-cov",
"pytest-django",
+ "pytest-randomly",
"pytest-xdist[psutil]",
"pytest",
- "pytest-randomly",
]
[tool.pytest.ini_options]
diff --git a/templates/twitch/game_detail.html b/templates/twitch/game_detail.html
index 940b883..31f66af 100644
--- a/templates/twitch/game_detail.html
+++ b/templates/twitch/game_detail.html
@@ -24,7 +24,7 @@
{% if game.box_art_best_url %}
- {% picture game.box_art_best_url alt=game.name width=160 %}
+ {% picture game.box_art_best_url alt="Box art for "|add:game.get_game_name width=160 %}
{% endif %}
diff --git a/twitch/feeds.py b/twitch/feeds.py
index 6a8678c..498db75 100644
--- a/twitch/feeds.py
+++ b/twitch/feeds.py
@@ -692,7 +692,7 @@ class OrganizationRSSFeed(TTVDropsBaseFeed):
feed_type = BrowserFriendlyRss201rev2Feed
title: str = "TTVDrops Twitch Organizations"
- link: str = "/organizations/"
+ link: str = "/twitch/organizations/"
description: str = "Latest organizations on TTVDrops"
_limit: int | None = None
@@ -921,7 +921,7 @@ class DropCampaignFeed(TTVDropsBaseFeed):
"""RSS feed for latest drop campaigns."""
title: str = "Twitch Drop Campaigns"
- link: str = "/campaigns/"
+ link: str = "/"
description: str = "Latest Twitch drop campaigns on TTVDrops"
item_guid_is_permalink = True
diff --git a/twitch/migrations/0007_rename_operation_name_to_operation_names.py b/twitch/migrations/0007_rename_operation_name_to_operation_names.py
index 55985e2..63190e1 100644
--- a/twitch/migrations/0007_rename_operation_name_to_operation_names.py
+++ b/twitch/migrations/0007_rename_operation_name_to_operation_names.py
@@ -5,7 +5,7 @@ from django.db import migrations
from django.db import models
-def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001, ARG001
+def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001
"""Convert operation_name string values to operation_names list."""
DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all():
@@ -14,7 +14,7 @@ def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001
campaign.save(update_fields=["operation_names"])
-def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ARG001, ANN001
+def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ANN001
"""Convert operation_names list back to operation_name string (first item only)."""
DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all():
diff --git a/twitch/migrations/0016_mark_all_drops_fully_imported.py b/twitch/migrations/0016_mark_all_drops_fully_imported.py
index 9fd5630..54b4003 100644
--- a/twitch/migrations/0016_mark_all_drops_fully_imported.py
+++ b/twitch/migrations/0016_mark_all_drops_fully_imported.py
@@ -1,7 +1,7 @@
from django.db import migrations
-def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001, ARG001
+def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001
"""Marks all existing DropCampaigns as fully imported.
This was needed to ensure that the Twitch API view only returns campaigns that are ready for display.
diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py
index b69e9cb..afd7a83 100644
--- a/twitch/tests/test_feeds.py
+++ b/twitch/tests/test_feeds.py
@@ -1410,3 +1410,33 @@ class DiscordFeedTestCase(TestCase):
f"Expected Discord timestamp format <t:UNIX_TIMESTAMP:R> in content, got: {content}"
)
assert "()" not in content
+
+ def test_feed_links_return_200(self) -> None:
+ """Test that all links in the feeds return 200 OK."""
+ feed_urls: list[str] = [
+ reverse("core:organization_feed"),
+ reverse("core:game_feed"),
+ reverse("core:organization_feed_atom"),
+ ]
+
+ for feed_url in feed_urls:
+ response: _MonkeyPatchedWSGIResponse = self.client.get(feed_url)
+ assert response.status_code == 200, f"Feed {feed_url} did not return 200."
+
+ # Extract all links from the feed content
+ content: str = response.content.decode("utf-8")
+ links: list[str] = re.findall(r'href=["\'](https?://.*?)["\']', content)
+
+ for link in links:
+ skip_links: list[str] = [
+ "rss_styles.xslt",
+ "twitch.tv",
+ ]
+
+ if any(skip in link for skip in skip_links):
+ # Skip testing rss_styles.xslt, external Twitch links, and internal links that may not exist in test environment
+ continue
+ link_response: _MonkeyPatchedWSGIResponse = self.client.get(link)
+
+ msg: str = f"Link {link} in feed {feed_url} did not return 200, got {link_response.status_code}."
+ assert link_response.status_code == 200, msg
diff --git a/twitch/views.py b/twitch/views.py
index 595bd42..2b492f8 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -1883,7 +1883,7 @@ def export_campaigns_json(request: HttpRequest) -> HttpResponse:
return response
-def export_games_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # noqa: ARG001
+def export_games_csv(request: HttpRequest) -> HttpResponse:
"""Export games to CSV format.
Args:
@@ -1923,7 +1923,7 @@ def export_games_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # n
return response
-def export_games_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 # noqa: ARG001
+def export_games_json(request: HttpRequest) -> HttpResponse:
"""Export games to JSON format.
Args:
@@ -1958,7 +1958,7 @@ def export_games_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001 #
return response
-def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG001
+def export_organizations_csv(request: HttpRequest) -> HttpResponse:
"""Export organizations to CSV format.
Args:
@@ -1987,7 +1987,7 @@ def export_organizations_csv(request: HttpRequest) -> HttpResponse: # noqa: ARG
return response
-def export_organizations_json(request: HttpRequest) -> HttpResponse: # noqa: ARG001
+def export_organizations_json(request: HttpRequest) -> HttpResponse:
"""Export organizations to JSON format.
Args: