fix(terrain): расширить whitelist endpoint'а на tri (ET-013 review F-1)
All checks were successful
All checks were successful
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) <noreply@anthropic.com>
This commit is contained in:
19
CHANGELOG.md
19
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 —
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user