From 2d1795dd280afb86cd7ca4a5a4c8178aa08da31c Mon Sep 17 00:00:00 2001 From: xzawed <47019855+xzawed@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:38:48 +0900 Subject: [PATCH] =?UTF-8?q?test(python):=20=ED=86=B5=ED=95=A9=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=EC=9D=98=20Keycloak=20=EC=A4=80=EB=B9=84=20=ED=83=90?= =?UTF-8?q?=EC=B9=A8=EC=9D=B4=20=EC=A3=BC=20=ED=8F=AC=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?404=20=EB=A5=BC=20=EB=8B=A4=EC=8B=9C=20=EB=AC=BB=EA=B2=8C=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20=E2=80=94=20=EA=B8=B0=EB=8F=99=20=EA=B2=BD?= =?UTF-8?q?=ED=95=A9=EC=9C=BC=EB=A1=9C=20=EC=84=B8=EC=85=98=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EA=B0=80=20setup=20=EC=98=A4=EB=A5=98=EC=98=80?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testcontainers 의 `_readiness_probe` 는 관리 포트(9000)가 연결 오류면 주 포트(8080)의 `/health/ready` 로 넘어가는데, Keycloak 25+ 의 주 포트에는 health 가 없어 404 이고 `raise_for_status` 의 HTTPError 는 데코레이터가 재시도하지 않는다 (ConnectionError·ReadTimeout 만). 기동 중 주 포트가 먼저 열리면 그 순간 실패해 통합 시험 31 개가 setup 오류였다 (#898 의 CI 2026-10-29 — 실패 URL 이 주 포트 `localhost:32769/health/ready` · 같은 코드로 실패 잡만 다시 돌리니 통과). `KeycloakContainer` 를 상속해 404 면 관리 포트가 열릴 때까지(180 초 한도) 다시 묻는다 — 부모 탐침을 그대로 부르므로 testcontainers 4.14·4.15 어느 구조에도 맞는다. 로컬 Docker 로 통합 시험 31 개 통과(rc 0). ruff · format 깨끗. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R4jYS5GwFA8V5oAqjw8pMp --- python/tests/integration/conftest.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/python/tests/integration/conftest.py b/python/tests/integration/conftest.py index ea55ce10..af3a609f 100644 --- a/python/tests/integration/conftest.py +++ b/python/tests/integration/conftest.py @@ -7,6 +7,7 @@ from __future__ import annotations +import time import traceback from collections.abc import Awaitable, Callable, Iterator from dataclasses import dataclass, replace @@ -14,6 +15,7 @@ from urllib.parse import parse_qsl, urlencode, urlsplit import pytest +import requests from testcontainers.keycloak import KeycloakContainer from keycloak_sdk import KeycloakClient, KeycloakConfig @@ -21,6 +23,30 @@ _REALM_IMPORT_FILE = Path(__file__).parent / "it-realm-realm.json" +#: 준비 탐침이 404 를 다시 묻는 한도(초) — Keycloak 기동은 CI 에서 수십 초다. +_READINESS_SECONDS = 180.0 + + +class _Keycloak(KeycloakContainer): # type: ignore[misc] + """⚠️ testcontainers 의 준비 탐침은 관리 포트(9000)가 연결 오류면 주 포트(8080)의 + `/health/ready` 로 넘어가는데, Keycloak 25+ 의 주 포트에는 health 가 없어 404 이고 그 404 는 + 다시 묻지 않는다(`raise_for_status` — 데코레이터는 ConnectionError·ReadTimeout 만 재시도한다). + 기동 중 주 포트가 관리 포트보다 먼저 열리면 세션 전체가 setup 오류로 끝났다(CI 실측 + 2026-10-29: 31 개가 그 URL 의 404 · 같은 코드로 실패 잡만 다시 돌리니 통과). 404 면 관리 + 포트가 열릴 때까지 다시 묻는다.""" + + def _readiness_probe(self) -> None: + deadline = time.monotonic() + _READINESS_SECONDS + while True: + try: + super()._readiness_probe() + return + except requests.exceptions.HTTPError: + if time.monotonic() > deadline: + raise + time.sleep(1) + + # 인가 코드 흐름용 — realm JSON 의 `it-web`(RS256)·`it-web-hs256`(id_token 을 HS256 서명)과 짝. # ⚠️ `it-web` 의 audience 매퍼는 introspect 용이다 — `aud` 가 없는 접근 토큰을 Keycloak 26.6 은 # 발급한 그 클라이언트가 물어도 `{"active": false}` 로 답한다(실측; aud=it-web 을 넣으면 true). @@ -99,7 +125,7 @@ def strip_nonce(request: AuthorizationUrl) -> AuthorizationUrl: def keycloak_url() -> Iterator[str]: """`it-realm`을 임포트한 Keycloak 26.6 컨테이너를 세션 동안 1회 기동하고 base auth-server URL(예: `http://localhost:32819`)을 반환한다.""" - container = KeycloakContainer("quay.io/keycloak/keycloak:26.6").with_realm_import_file( + container = _Keycloak("quay.io/keycloak/keycloak:26.6").with_realm_import_file( str(_REALM_IMPORT_FILE) ) with container as started: