# Search Console 클라이언트 `solution/backend/services/search_console_client.py` — Google Search Console 에 사이트맵을 제출하고 URL 색인 상태를 조회하는 REST 클라이언트만 다룬다. DB 저장·스케줄링·발행 감지·환경 설정은 [SEARCH_CONSOLE.md](SEARCH_CONSOLE.md)를 따른다. 공식 문서: [Sitemaps.submit](https://developers.google.com/webmaster-tools/v1/sitemaps/submit) · [urlInspection.index.inspect](https://developers.google.com/webmaster-tools/v1/urlInspection.index/inspect) ## 1. 계약 ```python class SearchConsoleClient: def __init__(self, credentials_file: str, *, transport: httpx.AsyncBaseTransport | None = None): ... async def submit_sitemap(self, property_url: str, sitemap_url: str) -> None: ... async def inspect_url(self, property_url: str, page_url: str) -> dict: ... # indexStatusResult 만 async def aclose(self) -> None: ... # async with SearchConsoleClient(...) as client: ... ``` - `credentials_file`: 서비스 계정 JSON 키 파일 경로. - `transport`: 테스트에서 `httpx.MockTransport` 를 꽂는 자리 — 실제 Google 호출 없이 검증한다. - `inspect_url` 은 응답의 `inspectionResult.indexStatusResult` 만 돌려준다. 그 경로가 없거나(검사 실패) 빈 dict 면(실제 검사가 안 된 응답) `SearchConsoleError` 를 올린다 — **"미색인"으로 넘겨짚지 않는다.** ## 2. 인증 서비스 계정 JSON 키 파일 + scope `https://www.googleapis.com/auth/webmasters`. `google.oauth2.service_account` · `google.auth.transport.requests.Request` · `requests` 는 전부 함수 안에서 import 한다. 두 패키지는 백엔드 `requirements.txt`에 포함되어 있다. - 토큰은 클라이언트 인스턴스에 캐시된다(`credentials.valid` 인 동안 재사용, 매 호출 갱신하지 않는다). 동시 호출은 `asyncio.Lock` 으로 갱신을 한 번만 태운다. - 갱신은 `asyncio.to_thread` 로 별도 스레드에서 돈다. 내부 `requests.Session` 요청에는 타임아웃을 강제로 20초로 덮어씌운다(`Request.__call__` 기본값 120초를 무시) — 만료된 키·막힌 네트워크에서 무한정 걸리는 것을 막는다. ## 3. 오류 — `SearchConsoleError(code)` `code` 문자열 하나만 들고 다닌다. **Google 응답 본문·액세스 토큰·키 파일 내용·원본 예외 메시지는 절대 담지 않는다** — 로그·잡 상태·관리 화면 어디로 흘러도 안전하다. | code | 뜻 | |---|---| | `invalid_credentials_file` | 키 파일을 못 읽거나 형식이 잘못됨 | | `auth_failed` | 토큰 갱신 실패, 또는 갱신 후에도 토큰이 비어 있음 | | `unauthorized` | HTTP 401 | | `forbidden` | HTTP 403 | | `rate_limited` | HTTP 429 | | `server_error` | HTTP 5xx | | `http_` | 그 외 실패 상태코드 | | `timeout` | 요청 타임아웃 | | `transport_error` | 그 외 전송 실패(연결 끊김 등) | | `invalid_json` | 200 인데 본문이 JSON 이 아님 | | `missing_inspection_result` | 응답에 `inspectionResult` 가 없음 | | `missing_index_status_result` | `inspectionResult` 는 있는데 `indexStatusResult` 가 없거나 빈 dict | ## 4. 테스트 ```bash cd solution/backend APP_ENV=test .venv/bin/python -m pytest tests/test_search_console_client.py --confcutdir=tests ``` `--confcutdir=tests` 가 필요한 이유: 저장소 루트 `conftest.py` 의 세션 스코프 autouse 픽스처가 실 Postgres 연결을 요구한다(`solution/backend/conftest.py`). 이 클라이언트 테스트는 DB 를 전혀 쓰지 않으므로 그 픽스처를 건너뛴다 — `--confcutdir=tests` 로 상위 `conftest.py` 탐색을 끊는다. (통합 후 전체 스위트를 돌릴 때는 이 플래그 없이 실행한다.) Google 실 API 는 전부 `httpx.MockTransport` 로 막았다 — 네트워크 호출도, 과금도 없다.