diff --git a/core/tests/test_sitemaps.py b/core/tests/test_sitemaps.py new file mode 100644 index 0000000..4486791 --- /dev/null +++ b/core/tests/test_sitemaps.py @@ -0,0 +1,73 @@ +import xml.etree.ElementTree as ET # noqa: S405 +from typing import TYPE_CHECKING + +from django.urls import reverse + +if TYPE_CHECKING: + from django.test.client import Client + + +def _extract_locs(xml_bytes: bytes) -> list[str]: + root = ET.fromstring(xml_bytes) # noqa: S314 + ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"} + return [el.text for el in root.findall(".//s:loc", ns) if el.text] + + +def test_sitemap_static_contains_expected_links( + client: Client, + settings: object, +) -> None: + """Ensure the static sitemap contains the main site links across apps. + + This test checks a representative set of URLs from core, twitch, kick, and + youtube apps as well as some misc static files like /robots.txt. + """ + # Ensure deterministic BASE_URL + settings.BASE_URL = "https://ttvdrops.lovinator.space" + + response = client.get(reverse("sitemap-static")) + assert response.status_code == 200 + assert response["Content-Type"] == "application/xml" + + locs = _extract_locs(response.content) + + base = settings.BASE_URL.rstrip("/") + + expected_paths = [ + reverse("core:dashboard"), + reverse("core:search"), + reverse("core:debug"), + reverse("core:dataset_backups"), + reverse("core:docs_rss"), + reverse("core:campaign_feed"), + reverse("core:game_feed"), + reverse("core:organization_feed"), + reverse("core:reward_campaign_feed"), + reverse("core:campaign_feed_atom"), + reverse("core:campaign_feed_discord"), + reverse("twitch:dashboard"), + reverse("twitch:campaign_list"), + reverse("twitch:games_grid"), + reverse("twitch:games_list"), + reverse("twitch:channel_list"), + reverse("twitch:badge_list"), + reverse("twitch:emote_gallery"), + reverse("twitch:org_list"), + reverse("twitch:reward_campaign_list"), + reverse("twitch:export_campaigns_csv"), + reverse("kick:dashboard"), + reverse("kick:campaign_list"), + reverse("kick:game_list"), + reverse("kick:organization_list"), + reverse("kick:campaign_feed"), + reverse("kick:game_feed"), + reverse("kick:organization_feed"), + reverse("youtube:index"), + "/about/", + "/robots.txt", + ] + + expected: set[str] = {base + p for p in expected_paths} + + for url in expected: + assert url in locs, f"Expected {url} in sitemap, found {len(locs)} entries" diff --git a/core/views.py b/core/views.py index bcba39e..464ded6 100644 --- a/core/views.py +++ b/core/views.py @@ -256,14 +256,59 @@ def sitemap_static_view(request: HttpRequest) -> HttpResponse: HttpResponse: The rendered sitemap XML. """ # `request` is unused but required by Django's view signature. - base_url: str = _build_base_url() + base_url: str = _build_base_url().rstrip("/") sitemap_urls: list[dict[str, str]] = [ - {"loc": f"{base_url}/"}, - {"loc": f"{base_url}/twitch/"}, - {"loc": f"{base_url}/twitch/campaigns/"}, - {"loc": f"{base_url}/twitch/games/"}, - {"loc": f"{base_url}/kick/"}, - {"loc": f"{base_url}/youtube/"}, + {"loc": f"{base_url}{reverse('core:dashboard')}"}, + {"loc": f"{base_url}{reverse('core:search')}"}, + {"loc": f"{base_url}{reverse('core:debug')}"}, + {"loc": f"{base_url}{reverse('core:dataset_backups')}"}, + {"loc": f"{base_url}{reverse('core:docs_rss')}"}, + # Core RSS/Atom/Discord feeds + {"loc": f"{base_url}{reverse('core:campaign_feed')}"}, + {"loc": f"{base_url}{reverse('core:game_feed')}"}, + {"loc": f"{base_url}{reverse('core:organization_feed')}"}, + {"loc": f"{base_url}{reverse('core:reward_campaign_feed')}"}, + {"loc": f"{base_url}{reverse('core:campaign_feed_atom')}"}, + {"loc": f"{base_url}{reverse('core:game_feed_atom')}"}, + {"loc": f"{base_url}{reverse('core:organization_feed_atom')}"}, + {"loc": f"{base_url}{reverse('core:reward_campaign_feed_atom')}"}, + {"loc": f"{base_url}{reverse('core:campaign_feed_discord')}"}, + {"loc": f"{base_url}{reverse('core:game_feed_discord')}"}, + {"loc": f"{base_url}{reverse('core:organization_feed_discord')}"}, + {"loc": f"{base_url}{reverse('core:reward_campaign_feed_discord')}"}, + # Twitch app pages + {"loc": f"{base_url}{reverse('twitch:dashboard')}"}, + {"loc": f"{base_url}{reverse('twitch:campaign_list')}"}, + {"loc": f"{base_url}{reverse('twitch:games_grid')}"}, + {"loc": f"{base_url}{reverse('twitch:games_list')}"}, + {"loc": f"{base_url}{reverse('twitch:channel_list')}"}, + {"loc": f"{base_url}{reverse('twitch:badge_list')}"}, + {"loc": f"{base_url}{reverse('twitch:emote_gallery')}"}, + {"loc": f"{base_url}{reverse('twitch:org_list')}"}, + {"loc": f"{base_url}{reverse('twitch:reward_campaign_list')}"}, + {"loc": f"{base_url}{reverse('twitch:export_campaigns_csv')}"}, + {"loc": f"{base_url}{reverse('twitch:export_games_csv')}"}, + {"loc": f"{base_url}{reverse('twitch:export_organizations_csv')}"}, + {"loc": f"{base_url}{reverse('twitch:export_campaigns_json')}"}, + {"loc": f"{base_url}{reverse('twitch:export_games_json')}"}, + {"loc": f"{base_url}{reverse('twitch:export_organizations_json')}"}, + # Kick app pages and feeds + {"loc": f"{base_url}{reverse('kick:dashboard')}"}, + {"loc": f"{base_url}{reverse('kick:campaign_list')}"}, + {"loc": f"{base_url}{reverse('kick:game_list')}"}, + {"loc": f"{base_url}{reverse('kick:organization_list')}"}, + {"loc": f"{base_url}{reverse('kick:campaign_feed')}"}, + {"loc": f"{base_url}{reverse('kick:game_feed')}"}, + {"loc": f"{base_url}{reverse('kick:organization_feed')}"}, + {"loc": f"{base_url}{reverse('kick:campaign_feed_atom')}"}, + {"loc": f"{base_url}{reverse('kick:game_feed_atom')}"}, + {"loc": f"{base_url}{reverse('kick:organization_feed_atom')}"}, + {"loc": f"{base_url}{reverse('kick:campaign_feed_discord')}"}, + {"loc": f"{base_url}{reverse('kick:game_feed_discord')}"}, + {"loc": f"{base_url}{reverse('kick:organization_feed_discord')}"}, + # YouTube + {"loc": f"{base_url}{reverse('youtube:index')}"}, + # Misc/static {"loc": f"{base_url}/about/"}, {"loc": f"{base_url}/robots.txt"}, ]