From 3687e91f7f7c8e1700bcd05415d5b38ffd310c94 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 4 Aug 2026 22:01:22 +0000 Subject: [PATCH] test(seo): add unit tests for sitemap export and API endpoints - test_sitemap_export.py: 19 tests covering SEOMAP_ROUTES constants, build_sitemap_xml() output structure, XML validity, trailing slash handling, and build_robots_txt() rules + sitemap reference - test_sitemap_api.py: 7 tests covering /sitemap.xml and /robots.txt endpoint status codes, content types, content validation, and auth-gated /regenerate endpoint Addresses reviewer REQUEST_CHANGES: 'This needs a unit test' and 'Unit test for this type too' --- backend/tests/test_sitemap_api.py | 55 +++++++++++++ backend/tests/test_sitemap_export.py | 113 +++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 backend/tests/test_sitemap_api.py create mode 100644 backend/tests/test_sitemap_export.py diff --git a/backend/tests/test_sitemap_api.py b/backend/tests/test_sitemap_api.py new file mode 100644 index 0000000..40720c4 --- /dev/null +++ b/backend/tests/test_sitemap_api.py @@ -0,0 +1,55 @@ +"""Tests for sitemap API endpoints — /api/sitemap/sitemap.xml, /api/sitemap/robots.txt, /api/sitemap/regenerate.""" + +import pytest +from httpx import AsyncClient +from starlette.testclient import TestClient + +from app.main import app + + +class TestSitemapEndpoints: + """Tests for sitemap API routes.""" + + def _client(self): + return TestClient(app) + + def test_sitemap_xml_status(self): + client = self._client() + resp = client.get("/api/sitemap/sitemap.xml") + assert resp.status_code == 200 + + def test_sitemap_xml_content_type(self): + client = self._client() + resp = client.get("/api/sitemap/sitemap.xml") + assert resp.headers["content-type"].startswith("application/xml") + + def test_sitemap_xml_contains_routes(self): + client = self._client() + resp = client.get("/api/sitemap/sitemap.xml") + body = resp.text + for path in ["/", "/about", "/schedule", "/events", "/contact"]: + assert f"https://kmountainflower.org{path}" in body + + def test_robots_txt_status(self): + client = self._client() + resp = client.get("/api/sitemap/robots.txt") + assert resp.status_code == 200 + + def test_robots_txt_content_type(self): + client = self._client() + resp = client.get("/api/sitemap/robots.txt") + assert resp.headers["content-type"].startswith("text/plain") + + def test_robots_txt_contains_rules(self): + client = self._client() + resp = client.get("/api/sitemap/robots.txt") + body = resp.text + assert "User-agent: *" in body + assert "Disallow: /admin" in body + assert "Disallow: /login" in body + assert "Sitemap: https://kmountainflower.org/sitemap.xml" in body + + def test_regenerate_requires_auth(self): + client = self._client() + resp = client.post("/api/sitemap/regenerate") + assert resp.status_code in (401, 403) diff --git a/backend/tests/test_sitemap_export.py b/backend/tests/test_sitemap_export.py new file mode 100644 index 0000000..4b5f6f8 --- /dev/null +++ b/backend/tests/test_sitemap_export.py @@ -0,0 +1,113 @@ +"""Tests for sitemap_export — sitemap.xml and robots.txt generation.""" + +import pytest +from xml.etree import ElementTree as ET + +from app.sitemap_export import ( + SEOMAP_ROUTES, + build_sitemap_xml, + build_robots_txt, +) + + +class TestSitemapRoutes: + """Verify the route configuration constants.""" + + def test_route_count(self): + assert len(SEOMAP_ROUTES) == 5 + + def test_home_route(self): + assert SEOMAP_ROUTES[0] == { + "path": "/", + "changefreq": "daily", + "priority": 1.0, + } + + def test_all_paths_present(self): + paths = [r["path"] for r in SEOMAP_ROUTES] + expected = ["/", "/about", "/schedule", "/events", "/contact"] + assert paths == expected + + def test_priority_range(self): + for route in SEOMAP_ROUTES: + assert 0.0 <= route["priority"] <= 1.0 + + def test_valid_changefreq_values(self): + valid = {"always", "hourly", "daily", "weekly", "monthly", "yearly", "never"} + for route in SEOMAP_ROUTES: + assert route["changefreq"] in valid + + +class TestBuildSitemapXml: + """Tests for the sitemap XML builder.""" + + def test_returns_string(self): + result = build_sitemap_xml("https://example.com") + assert isinstance(result, str) + + def test_contains_xml_declaration(self): + result = build_sitemap_xml("https://example.com") + assert '' in result + + def test_contains_urlset_namespace(self): + result = build_sitemap_xml("https://example.com") + assert 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' in result + + def test_contains_all_routes(self): + result = build_sitemap_xml("https://example.com") + for route in SEOMAP_ROUTES: + assert f"https://example.com{route['path']}" in result + + def test_strips_trailing_slash_from_base_url(self): + result = build_sitemap_xml("https://example.com/") + assert "https://example.com//about" not in result + assert "https://example.com/about" in result + + def test_different_base_url(self): + result = build_sitemap_xml("https://mystation.org") + assert "https://mystation.org/schedule" in result + assert "https://example.com" not in result + + def test_valid_xml(self): + result = build_sitemap_xml("https://example.com") + # Should parse without error + root = ET.fromstring(result) + # The root tag includes the namespace + assert "urlset" in root.tag + + def test_contains_changefreq_and_priority(self): + result = build_sitemap_xml("https://example.com") + assert "daily" in result + assert "1.0" in result + assert "0.5" in result + + +class TestBuildRobotsTxt: + """Tests for the robots.txt builder.""" + + def test_returns_string(self): + result = build_robots_txt("https://example.com") + assert isinstance(result, str) + + def test_user_agent_allows_all(self): + result = build_robots_txt("https://example.com") + assert "User-agent: *" in result + assert "Allow: /" in result + + def test_disallows_admin_and_login(self): + result = build_robots_txt("https://example.com") + assert "Disallow: /admin" in result + assert "Disallow: /login" in result + + def test_sitemap_reference(self): + result = build_robots_txt("https://example.com") + assert "Sitemap: https://example.com/sitemap.xml" in result + + def test_strips_trailing_slash(self): + result = build_robots_txt("https://example.com/") + assert "Sitemap: https://example.com//sitemap.xml" not in result + assert "Sitemap: https://example.com/sitemap.xml" in result + + def test_different_base_url(self): + result = build_robots_txt("https://mystation.org") + assert "Sitemap: https://mystation.org/sitemap.xml" in result