Тесты настроек NATS: fix-wave по minor-находкам ревью (T2.1, T2.2, T3.1, T3.2, лог старта)
- invalid_subject параметризован по трём ключам, equality покрывает три пары - кейс утечки JWT/seed при невалидном subject - /healthcheck с нестандартными subject'ами и проверкой отсутствия JWT - subscribe с настроенным subject'ом, лог старта с subject'ами - tasks.md: отмечены 1.1, 1.2, 2.x, 3.1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9d317b6260
commit
db8b0bcc13
@ -18,6 +18,7 @@ import pytest
|
||||
from fastapi import Request
|
||||
from fastapi.routing import APIRoute
|
||||
from loguru import logger
|
||||
from pydantic import SecretStr
|
||||
|
||||
from src.api.router import router
|
||||
from src.core.config import Settings
|
||||
@ -66,6 +67,30 @@ async def test_healthcheck_subjects(client: httpx.AsyncClient, monkeypatch: pyte
|
||||
assert "jwt" not in str(body).lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True], ids=["when_nats_off", "when_nats_down"])
|
||||
async def test_healthcheck_returns_non_default_subjects_without_jwt(
|
||||
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch, enabled: bool
|
||||
):
|
||||
"""T3.1: отдаются именно настроенные (не умолчательные) subject'ы; непустой JWT в тело не попадает."""
|
||||
jwt = "JWTVALUEMARK"
|
||||
nats = app_module.settings.OPENBAO.NATS
|
||||
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", enabled)
|
||||
monkeypatch.setattr(nats, "NATS_TABULAR_REQUEST_SUBJECT", "env.files.tabular_uploaded")
|
||||
monkeypatch.setattr(nats, "NATS_IMAGE_REQUEST_SUBJECT", "env.files.img_uploaded")
|
||||
monkeypatch.setattr(nats, "NATS_RESULT_SUBJECT", "env.files.tabular_parsed")
|
||||
monkeypatch.setattr(nats, "NATS_JWT", SecretStr(jwt))
|
||||
assert nats.NATS_JWT.get_secret_value() == jwt
|
||||
|
||||
response = await client.get("/healthcheck")
|
||||
|
||||
assert response.json()["subjects"] == {
|
||||
"tabular_request": "env.files.tabular_uploaded",
|
||||
"image_request": "env.files.img_uploaded",
|
||||
"result": "env.files.tabular_parsed",
|
||||
}
|
||||
assert jwt not in response.text
|
||||
|
||||
|
||||
async def test_healthcheck_nats_down_when_enabled_without_connection(
|
||||
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
@ -239,6 +264,26 @@ async def test_lifespan_does_not_wait_for_nats_and_healthcheck_is_down(monkeypat
|
||||
assert not nats.is_connected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("_no_smb")
|
||||
async def test_lifespan_logs_nats_subjects_at_start(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Tasks 1.2: при старте в лог пишутся используемые subject'ы NATS."""
|
||||
records: list[Any] = []
|
||||
handler_id = logger.add(lambda message: records.append(message.record), level="INFO")
|
||||
monkeypatch.setattr(app_module, "_connect_s3", _unavailable_s3)
|
||||
monkeypatch.setattr(app_module, "_create_nats", lambda: None)
|
||||
monkeypatch.setattr(app_module.settings.OPENBAO.NATS, "NATS_RESULT_SUBJECT", "env.files.tabular_parsed")
|
||||
try:
|
||||
async with app_module.lifespan(app_module.app):
|
||||
pass
|
||||
finally:
|
||||
logger.remove(handler_id)
|
||||
|
||||
messages = [r["message"] for r in records if "Subject'ы NATS" in r["message"]]
|
||||
assert len(messages) == 1
|
||||
assert "env.files.tabular_parsed" in messages[0]
|
||||
assert "files.tabular_uploaded" in messages[0]
|
||||
|
||||
|
||||
def test_create_nats_disabled_returns_none(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(app_module.settings, "NATS_ENABLED", False)
|
||||
assert app_module._create_nats() is None
|
||||
|
||||
@ -184,18 +184,44 @@ def test_nats_subjects_from_openbao(settings_from_env) -> None:
|
||||
assert nats.NATS_RESULT_SUBJECT == "test.files.tabular_parsed"
|
||||
|
||||
|
||||
SUBJECT_KEYS = ("NATS_TABULAR_REQUEST_SUBJECT", "NATS_IMAGE_REQUEST_SUBJECT", "NATS_RESULT_SUBJECT")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", SUBJECT_KEYS)
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
["", "files.*", "files.>", ">", "files..tabular", ".files", "files.", "files tabular", "files.tabular_uploaded "],
|
||||
)
|
||||
def test_invalid_subject_rejected(settings_from_env, value: str) -> None:
|
||||
with pytest.raises(ValidationError, match="NATS_RESULT_SUBJECT"):
|
||||
settings_from_env(OPENBAO__NATS__NATS_RESULT_SUBJECT=value)
|
||||
def test_invalid_subject_rejected(settings_from_env, key: str, value: str) -> None:
|
||||
with pytest.raises(ValidationError, match=key):
|
||||
settings_from_env(**{f"OPENBAO__NATS__{key}": value})
|
||||
|
||||
|
||||
def test_equal_subjects_rejected(settings_from_env) -> None:
|
||||
with pytest.raises(ValidationError, match="NATS_TABULAR_REQUEST_SUBJECT и NATS_RESULT_SUBJECT совпадают"):
|
||||
settings_from_env(OPENBAO__NATS__NATS_RESULT_SUBJECT="files.tabular_uploaded")
|
||||
@pytest.mark.parametrize(
|
||||
("first", "second"),
|
||||
[
|
||||
("NATS_TABULAR_REQUEST_SUBJECT", "NATS_IMAGE_REQUEST_SUBJECT"),
|
||||
("NATS_TABULAR_REQUEST_SUBJECT", "NATS_RESULT_SUBJECT"),
|
||||
("NATS_IMAGE_REQUEST_SUBJECT", "NATS_RESULT_SUBJECT"),
|
||||
],
|
||||
)
|
||||
def test_equal_subjects_rejected(settings_from_env, first: str, second: str) -> None:
|
||||
with pytest.raises(ValidationError, match=f"{first} и {second} совпадают"):
|
||||
settings_from_env(**{f"OPENBAO__NATS__{first}": "files.same", f"OPENBAO__NATS__{second}": "files.same"})
|
||||
|
||||
|
||||
def test_invalid_subject_error_hides_nats_secrets(settings_from_env) -> None:
|
||||
"""Ошибка валидации subject'а при заданных JWT/seed не печатает их (маркеры короткие, см. тест ниже)."""
|
||||
jwt_mark, seed_mark = "JWTMARK", "SEEDMARK"
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
settings_from_env(
|
||||
OPENBAO__NATS__NATS_JWT=jwt_mark,
|
||||
OPENBAO__NATS__NATS_SEED=seed_mark,
|
||||
OPENBAO__NATS__NATS_RESULT_SUBJECT="files.*",
|
||||
)
|
||||
assert "NATS_RESULT_SUBJECT" in str(exc_info.value)
|
||||
assert jwt_mark not in str(exc_info.value)
|
||||
assert seed_mark not in str(exc_info.value)
|
||||
|
||||
|
||||
def test_nats_secrets_hidden_in_repr(settings_from_env) -> None:
|
||||
|
||||
@ -393,3 +393,18 @@ def test_connection_subscribes_to_configured_result_subject() -> None:
|
||||
nats = NATSSettings(NATS_URL="nats://x:4222", NATS_RESULT_SUBJECT="test.files.tabular_parsed")
|
||||
assert NatsConnection(nats).result_subject == "test.files.tabular_parsed"
|
||||
assert NatsConnection(nats, result_subject="override").result_subject == "override"
|
||||
|
||||
|
||||
async def test_connection_subscribes_with_configured_result_subject() -> None:
|
||||
"""T3.2: subscribe вызывается с subject'ом из настроек (без override) и группой очереди."""
|
||||
nats = NATSSettings(NATS_URL="nats://x:4222", NATS_RESULT_SUBJECT="test.files.tabular_parsed")
|
||||
world = _FakeWorld()
|
||||
conn = NatsConnection(nats, client_factory=world, sleep=_RecordingSleep(), queue="g")
|
||||
await conn.start()
|
||||
assert await conn.wait_ready(timeout=2)
|
||||
|
||||
await conn.subscribe_results(_handler)
|
||||
|
||||
subscription = world.clients[-1].subscriptions[0]
|
||||
assert (subscription["subject"], subscription["queue"]) == ("test.files.tabular_parsed", "g")
|
||||
await conn.stop()
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
## 1. Тесты
|
||||
|
||||
- [ ] 1.1 Unit: умолчания subject'ов, чтение `OPENBAO__NATS__NATS_*_SUBJECT`, отказ на `*`/`>`/пробеле/пустом токене и
|
||||
- [x] 1.1 Unit: умолчания subject'ов, чтение `OPENBAO__NATS__NATS_*_SUBJECT`, отказ на `*`/`>`/пробеле/пустом токене и
|
||||
совпадающих именах; `SecretStr` для `NATS_JWT`/`NATS_SEED`, отсутствие значений в ошибке валидации и `repr`
|
||||
- [ ] 1.2 Unit: `/healthcheck.subjects` (`tabular_request`, `image_request`, `result`) при `nats = ok | down | off`; лог старта с subject'ами
|
||||
- [x] 1.2 Unit: `/healthcheck.subjects` (`tabular_request`, `image_request`, `result`) при `nats = ok | down | off`; лог старта с subject'ами
|
||||
- [ ] 1.3 Интеграционный тест: приложение с subject'ами окружения публикует запрос и подписано на ответ по ним
|
||||
|
||||
## 2. Реализация
|
||||
|
||||
- [ ] 2.1 `core/config.py`: поля и валидация `NATSSettings`, `SecretStr`, `hide_input_in_errors=True`
|
||||
- [ ] 2.2 Колбэки nkeys — `get_secret_value()`
|
||||
- [ ] 2.3 Подписка и публикация по настройкам; константы `contract/subjects.py` — умолчания
|
||||
- [ ] 2.4 `/healthcheck.subjects`, лог при старте
|
||||
- [x] 2.1 `core/config.py`: поля и валидация `NATSSettings`, `SecretStr`, `hide_input_in_errors=True`
|
||||
- [x] 2.2 Колбэки nkeys — `get_secret_value()`
|
||||
- [x] 2.3 Подписка и публикация по настройкам; константы `contract/subjects.py` — умолчания
|
||||
- [x] 2.4 `/healthcheck.subjects`, лог при старте
|
||||
|
||||
## 3. Документы
|
||||
|
||||
- [ ] 3.1 `api/.env.example`, README (переменные NATS, `/healthcheck`)
|
||||
- [x] 3.1 `api/.env.example`, README (переменные NATS, `/healthcheck`)
|
||||
- [ ] 3.2 `make check-full` зелёный
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user