From 099669deeba4b70a5c17703ef49fd351af5712d0 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Thu, 4 Jun 2026 09:59:51 +0000 Subject: [PATCH] =?UTF-8?q?fix(terrain):=20=D1=80=D0=B0=D1=81=D1=88=D0=B8?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D1=8C=20whitelist=20endpoint'=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20`tri`=20(ET-013=20review=20F-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer'ом найден pre-existing P1: backend `terrain_tile` whitelist не пропускал слой `tri`, хотя фронтенд (`onTerrainCheckbox`) шлёт запросы на `/terrain/tri/{z}/{x}/{y}.png` для слоя «Перепады высот». На test/prod-среде эти запросы перехватывает nginx (подтверждено эмпирически — 404 идёт с сигнатурой `nginx/1.18.0 (Ubuntu)`, а не с FastAPI JSON-detail), но в dev-режиме (`make dev` → FastAPI на :5556 напрямую) endpoint обязан поддерживать `tri` нативно. Изменения: - `src/api/main.py:1252`: whitelist `("hypso", "hillshade")` → `("hypso", "hillshade", "tri")`. Ответ-контракт и заголовки идентичны существующим слоям; REQ-F-18 «API contract без изменений» не нарушен (поведение для уже-известных layer'ов не меняется, добавляется только поддержка нового layer'а). - `tests/integration/test_terrain_z9_tiles.py`: новый параметризованный тест `test_known_terrain_layer_accepted_by_whitelist[hypso|hillshade|tri]`, фиксирующий регрессию F-1 (не требует локальных PNG-данных: для несуществующего файла ожидает `detail: "Tile not found"`, а не `"Unknown layer"`). - `tests/integration/test_terrain_z9_tiles.py`: параметризация `test_terrain_tile_available_z9_z10_z11` по `(layer × zoom)` — 6 кейсов вместо 3 (review F-2). - `tests/integration/test_terrain_z9_tiles.py`: убран неиспользуемый `from __future__ import annotations` (review F-4); type-аннотации упрощены (Python 3.10+ нативно). - `tests/integration/test_terrain_z9_tiles.py`: `test_unknown_terrain_layer_returns_404` усилен ассертом `detail == "Unknown layer"` (парность с whitelist-тестом). Тесты: 17/17 unit PASS, 6/6 non-data-зависимых integration PASS, 6 layer×zoom кейсов SKIPPED (нет PH-6 данных в sandbox — корректное поведение `_maybe_skip`). Refs: ET-013, review F-1/F-2/F-4 (`docs/work-items/ET-013/12-review.md`). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 19 ++++++++ src/api/main.py | 13 +++++- tests/integration/test_terrain_z9_tiles.py | 52 +++++++++++++++++----- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2537081..80b4dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,26 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [Unreleased] +### Added +- ET-013 (review F-1 fix): Слой `tri` (Terrain Ruggedness Index) добавлен + в whitelist FastAPI-endpoint'а `GET /terrain/{layer}/{z}/{x}/{y}.png` + (`src/api/main.py`). На test/prod-среде nginx перехватывает + `/enduro/terrain/*` и отдаёт PNG напрямую с диска (подтверждено эмпирически + по 404-сигнатуре `nginx/1.18.0`), но в dev-режиме (`make dev` → + FastAPI на :5556 без nginx) endpoint должен поддерживать `tri` нативно. + Изменение аддитивное: ответ-контракт и заголовки идентичны существующим + слоям (`hypso`, `hillshade`); REQ-F-18 «API contract без изменений» + не нарушен. Регрессия: integration-тест `test_known_terrain_layer_accepted_by_whitelist` + параметризован по `(hypso, hillshade, tri)` и проверяет, что для + заведомо отсутствующего файла возвращается `detail: "Tile not found"`, + а не `"Unknown layer"`. Refs: ET-013, review F-1. + ### Changed +- ET-013 (review F-2 fix): Integration-тест + `tests/integration/test_terrain_z9_tiles.py` параметризован по + `(layer ∈ {hillshade, tri}) × (zoom ∈ {9, 10, 11})` — 6 кейсов + вместо 3, покрывает оба слоя на расширенном диапазоне зумов + (ранее покрывался только `hillshade`). Refs: ET-013, review F-2. - ET-012: Слой публичных GPS-треков теперь виден с зума z=5 (раньше — с z=8). Калибровка существующей tier-структуры `build_gps_mvt`/`_simplify_coords` (ADR-016): для z≤5 фильтр `min_length=10 км`, `limit=1500`; для z=6 — diff --git a/src/api/main.py b/src/api/main.py index d7dd216..43ac89d 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -1239,8 +1239,17 @@ TERRAIN_DIR = os.environ.get( @app.get("/terrain/{layer}/{z}/{x}/{y}.png") async def terrain_tile(layer: str, z: int, x: int, y: int): - """Отдаёт растровые тайлы рельефа (hypso/hillshade)""" - if layer not in ("hypso", "hillshade"): + """Отдаёт растровые тайлы рельефа (hypso/hillshade/tri). + + ET-013: добавлен слой ``tri`` (Terrain Ruggedness Index) в whitelist. + Фронтенд (`src/web/app.js`, ``onTerrainCheckbox``) запрашивает + ``/terrain/tri/{z}/{x}/{y}.png`` для слоя «Перепады высот». На + test/prod-среде эти запросы перехватывает nginx и отдаёт PNG + напрямую с диска, но в dev-режиме (``make dev`` → FastAPI на :5556 + без nginx) endpoint должен поддерживать ``tri`` нативно. + См. review ET-013 F-1. + """ + if layer not in ("hypso", "hillshade", "tri"): raise HTTPException(404, "Unknown layer") tile_path = os.path.join(TERRAIN_DIR, layer, str(z), str(x), f"{y}.png") if not os.path.exists(tile_path): diff --git a/tests/integration/test_terrain_z9_tiles.py b/tests/integration/test_terrain_z9_tiles.py index 9094616..3a13ca0 100644 --- a/tests/integration/test_terrain_z9_tiles.py +++ b/tests/integration/test_terrain_z9_tiles.py @@ -11,13 +11,12 @@ они не требуют исходных данных. Покрытие тест-плана (`04-test-plan.yaml`): -- IT-TILE-Z9-01, IT-TILE-Z10-01, IT-TILE-Z11-01 +- IT-TILE-Z9-01, IT-TILE-Z10-01, IT-TILE-Z11-01 (по обоим слоям hillshade и tri — см. F-2) - IT-TILE-INVALID-LAYER, IT-TILE-MISSING - IT-TILE-CACHE-HEADER +- IT-TILE-TRI-WHITELIST: регрессия, что endpoint признаёт `tri` (см. review F-1) """ -from __future__ import annotations - import os from pathlib import Path @@ -38,7 +37,7 @@ def client() -> TestClient: return TestClient(app) -def _find_sample_tile(layer: str, z: int) -> tuple[int, int] | None: +def _find_sample_tile(layer: str, z: int): """Найти любую существующую (x, y) пару тайла для layer/z. Возвращает None, если данных нет — тогда вызывающий тест помечается skipped. @@ -64,7 +63,7 @@ def _find_sample_tile(layer: str, z: int) -> tuple[int, int] | None: return None -def _maybe_skip(layer: str, z: int) -> tuple[int, int]: +def _maybe_skip(layer: str, z: int): sample = _find_sample_tile(layer, z) if sample is None: pytest.skip( @@ -75,29 +74,60 @@ def _maybe_skip(layer: str, z: int) -> tuple[int, int]: # ────────────────────────────────────────────────────────────────────────────── -# IT-TILE-Z9 / Z10 / Z11 — hillshade доступен на расширенном диапазоне зумов +# IT-TILE-Z9 / Z10 / Z11 — hillshade и TRI доступны на расширенном диапазоне зумов +# (review F-2: параметризация по layer, чтобы покрыть оба слоя endpoint'а) # ────────────────────────────────────────────────────────────────────────────── +@pytest.mark.parametrize("layer", ["hillshade", "tri"]) @pytest.mark.parametrize("zoom", [9, 10, 11]) -def test_hillshade_tile_available_z9_z10_z11(client: TestClient, zoom: int): - """IT-TILE-Z9/Z10/Z11-01: hillshade-тайл на z9-z11 отдаётся 200 PNG.""" - x, y = _maybe_skip("hillshade", zoom) - resp = client.get(f"/terrain/hillshade/{zoom}/{x}/{y}.png") +def test_terrain_tile_available_z9_z10_z11(client: TestClient, layer: str, zoom: int): + """IT-TILE-Z9/Z10/Z11-01: тайл рельефа (hillshade и tri) на z9-z11 отдаётся 200 PNG.""" + x, y = _maybe_skip(layer, zoom) + resp = client.get(f"/terrain/{layer}/{zoom}/{x}/{y}.png") assert resp.status_code == 200, resp.text assert resp.headers["content-type"] == "image/png" assert len(resp.content) > 0 +# ────────────────────────────────────────────────────────────────────────────── +# IT-TILE-TRI-WHITELIST (review F-1) — endpoint признаёт слой `tri` +# Этот тест не зависит от наличия тайлов: для существующего слоя без файла на +# диске мы получаем 404 "Tile not found", а для несуществующего слоя — 404 +# "Unknown layer". Различие проверяется по телу ответа. +# ────────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("layer", ["hypso", "hillshade", "tri"]) +def test_known_terrain_layer_accepted_by_whitelist(client: TestClient, layer: str): + """Регрессия F-1: каждый из (hypso, hillshade, tri) проходит whitelist.""" + # x, y заведомо не существуют на диске → должны получить 404 "Tile not found", + # но НЕ "Unknown layer". Эта проверка работает без локальных PNG-данных. + resp = client.get(f"/terrain/{layer}/9/999999/999999.png") + assert resp.status_code == 404 + detail = resp.json().get("detail", "") + assert detail != "Unknown layer", ( + f"layer={layer!r} должен проходить whitelist, факт detail={detail!r}" + ) + assert detail == "Tile not found", ( + f"для несуществующего файла ожидался detail='Tile not found', факт={detail!r}" + ) + + # ────────────────────────────────────────────────────────────────────────────── # Регрессии 404 — работают независимо от наличия данных # ────────────────────────────────────────────────────────────────────────────── def test_unknown_terrain_layer_returns_404(client: TestClient): - """IT-TILE-INVALID-LAYER: неизвестный layer → 404.""" + """IT-TILE-INVALID-LAYER: неизвестный layer → 404 "Unknown layer". + + Парный к ``test_known_terrain_layer_accepted_by_whitelist`` (F-1): + подтверждает, что whitelist всё ещё отсекает посторонние слои. + """ resp = client.get("/terrain/unknown_layer/9/0/0.png") assert resp.status_code == 404 + assert resp.json().get("detail") == "Unknown layer" def test_missing_terrain_tile_returns_404(client: TestClient):