"""발행 URL 의 색인 통보(IndexNow). 이 경로가 절대 하면 안 되는 것: - 통보 실패로 발행을 되돌리는 것 — 정적 파일은 이미 올라갔다. 다음 발행에서 다시 보내면 된다. - 사이트맵에 없는 URL 을 통보하는 것 — 404 통보는 신뢰만 깎는다. """ from pathlib import Path import httpx import pytest from services import indexnow SITEMAP = """ https://w4ai.o2o.kr/s/butter/ https://w4ai.o2o.kr/s/butter/faq """ def _out(tmp_path: Path, slug: str, body: str = SITEMAP) -> Path: site = tmp_path / "s" / slug site.mkdir(parents=True) (site / "sitemap.xml").write_text(body) return tmp_path def test_키가_없으면_통보하지_않는다(monkeypatch): monkeypatch.delenv("INDEXNOW_KEY", raising=False) assert indexnow.is_configured() is False def test_보낼_URL_은_사이트맵에서_읽는다(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(_out(tmp_path, "butter"))) assert indexnow.site_urls("butter") == [ "https://w4ai.o2o.kr/s/butter/", "https://w4ai.o2o.kr/s/butter/faq", ] def test_사이트맵이_없으면_빈_목록(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(tmp_path)) assert indexnow.site_urls("없는사이트") == [] def test_깨진_사이트맵은_예외를_올리지_않는다(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(_out(tmp_path, "butter", "깨짐"))) assert indexnow.site_urls("butter") == [] @pytest.mark.asyncio async def test_키파일_위치와_호스트를_URL에서_뽑는다(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(_out(tmp_path, "butter"))) monkeypatch.setenv("INDEXNOW_KEY", "abc12345") sent = {} async def fake_post(self, url, json=None, **kwargs): sent["url"] = url sent["body"] = json return httpx.Response(200, request=httpx.Request("POST", url)) monkeypatch.setattr(httpx.AsyncClient, "post", fake_post) result = await indexnow.submit("butter") assert sent["url"] == indexnow.ENDPOINT assert sent["body"]["host"] == "w4ai.o2o.kr" assert sent["body"]["key"] == "abc12345" # 키 파일은 오리진 루트에 있다 — 사이트 경로(/s/butter) 아래가 아니다. assert sent["body"]["keyLocation"] == "https://w4ai.o2o.kr/abc12345.txt" assert sent["body"]["urlList"] == [ "https://w4ai.o2o.kr/s/butter/", "https://w4ai.o2o.kr/s/butter/faq", ] assert result == {"ok": True, "status": 200, "urls": 2} @pytest.mark.asyncio async def test_거절되어도_예외를_올리지_않는다(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(_out(tmp_path, "butter"))) monkeypatch.setenv("INDEXNOW_KEY", "abc12345") async def fake_post(self, url, json=None, **kwargs): # 403 = 키 파일 대조 실패. 발행은 이미 끝났으므로 되돌리지 않는다. return httpx.Response(403, text="key not found", request=httpx.Request("POST", url)) monkeypatch.setattr(httpx.AsyncClient, "post", fake_post) assert await indexnow.submit("butter") == {"ok": False, "status": 403, "urls": 2} @pytest.mark.asyncio async def test_네트워크_실패도_삼킨다(tmp_path, monkeypatch): monkeypatch.setenv("SITE_OUTPUT_DIR", str(_out(tmp_path, "butter"))) monkeypatch.setenv("INDEXNOW_KEY", "abc12345") async def fake_post(self, url, json=None, **kwargs): raise httpx.ConnectTimeout("timed out") monkeypatch.setattr(httpx.AsyncClient, "post", fake_post) result = await indexnow.submit("butter") assert result["ok"] is False assert "ConnectTimeout" in result["error"]