Files
enduro-trails/tests/api/test_gps_tracks_filename.py
claude-bot eea6c846c2
Some checks failed
CI / lint (push) Failing after 5s
CI / test (push) Failing after 6s
CI / build (push) Has been skipped
CI / lint (pull_request) Failing after 4s
CI / test (pull_request) Failing after 5s
CI / build (pull_request) Has been skipped
feat(gps-tracks): GPX download from public track popup
Реализация ET-011: кнопка «Скачать GPX» в popup публичного GPS-трека и
новый эндпоинт GET /api/gps-tracks/{track_id}/download (GPX 1.1 +
Content-Disposition с UTF-8 именем по RFC 5987). Реэкспорт защищён
per-source флагом `download_allowed` в `config/gps_sources.yaml`
(default-deny, MVP whitelist = `osm`).

Backend:
- `src/api/gps_tracks/export.py` — чистый stdlib-builder GPX 1.1
  (`build_gpx`) + санитизация имени файла (`safe_filename`, RFC 5987).
- `src/api/gps_tracks/endpoint.py` — новый route с проверками
  400 / 403 / 404 / 413; cap 200 000 точек (REQ-NF-02).
- `src/api/gps_tracks/config.py` — `load_download_allowed_sources()`
  читает YAML, default-deny при отсутствии поля; fallback на `{"osm"}`
  при отсутствии конфига.
- `src/api/main.py` — пробрасывает `GPS_SOURCES_CONFIG_PATH` в router.

Frontend:
- `src/web/gps_tracks.js` — кнопка в `_renderTrackPopupHtml`,
  обработчик `_downloadPublicTrack` (fetch + Blob + a.download — тот же
  паттерн, что в `app.js::downloadGPX`, R-1 митигирован), парсер
  `_parseFilenameFromCD` для RFC 5987, маппинг ошибок
  `_handleDownloadError` (403/404/413/5xx → showToast).
- `src/web/app.css` — стиль кнопки, 32×32 CSS px (REQ-NF-04).

Тесты:
- 13 unit для GPX-builder (UT-01/02/03/05; XSD-валидация против
  `tests/fixtures/gpx-1.1/gpx.xsd`).
- 10 unit для `safe_filename` (UT-04).
- 11 integration для download-эндпоинта (IT-01..08 +
  ANY-rule license check + default-deny без конфига).

ADR-014 (gpx-download-endpoint), ADR-015 (source-redistribution-policy).
Refs: ET-011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 20:59:53 +00:00

96 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit-тесты для ET-011 sanitize/safe_filename (UT-04, REQ-F-04)."""
from __future__ import annotations
import urllib.parse
from src.api.gps_tracks.export import safe_filename
def test_ut04_cyrillic_utf8():
"""UT-04: кириллическое имя → ascii-fallback пустой и читается из utf-8."""
name = "По грязи к Чёрному озеру"
ascii_fb, utf8_quoted = safe_filename(name, 42)
# ascii_fallback содержит только ASCII (после санитизации
# нелатинские символы стали '_'), длина ≤ 80
assert all(ord(c) < 128 for c in ascii_fb)
assert len(ascii_fb) <= 80
# decoded utf-8 совпадает с исходным именем (до триммирования по 80 байтам)
decoded = urllib.parse.unquote(utf8_quoted, encoding="utf-8")
assert decoded == name
def test_ut04_forbidden_chars_replaced():
"""UT-04: запрещённые ФС-символы → '_'."""
name = 'Trail/with:bad*chars?"<>|'
ascii_fb, _ = safe_filename(name, 1)
for ch in '/\\:*?"<>|':
assert ch not in ascii_fb
# должно быть несколько подчёркиваний (хотя бы один на запрещённый символ)
assert "_" in ascii_fb
def test_ut04_empty_name_fallback_track_id():
"""UT-04: пустое имя → 'track-<id>'."""
ascii_fb, utf8_q = safe_filename("", 42)
assert ascii_fb == "track-42"
assert urllib.parse.unquote(utf8_q) == "track-42"
def test_ut04_none_name_fallback_track_id():
ascii_fb, utf8_q = safe_filename(None, 7)
assert ascii_fb == "track-7"
assert urllib.parse.unquote(utf8_q) == "track-7"
def test_ut04_truncated_to_80_bytes():
"""UT-04: длинное ASCII-имя триммится до 80 байт."""
name = "X" * 200
ascii_fb, utf8_q = safe_filename(name, 1)
assert len(ascii_fb.encode("utf-8")) <= 80
# utf8_q после percent-decoding тоже не должен превышать лимит
decoded = urllib.parse.unquote(utf8_q, encoding="utf-8")
assert len(decoded.encode("utf-8")) <= 80
def test_ut04_truncated_utf8_no_broken_codepoints():
"""UT-04: триммирование multibyte-строки не ломает code-point."""
# 200 русских букв = 400 байт UTF-8; триммим до 80 байт → ~40 букв
name = "Я" * 200
ascii_fb, utf8_q = safe_filename(name, 1)
decoded = urllib.parse.unquote(utf8_q, encoding="utf-8")
# должно успешно декодироваться
assert len(decoded) > 0
assert len(decoded.encode("utf-8")) <= 80
def test_ut04_only_forbidden_chars_fallback():
"""UT-04: имя из одних запрещённых символов после strip → fallback."""
ascii_fb, utf8_q = safe_filename("...", 5)
# точки страйпятся, остаётся пустота → fallback
assert ascii_fb == "track-5"
def test_ut04_whitespace_only_fallback():
ascii_fb, _ = safe_filename(" ", 8)
assert ascii_fb == "track-8"
def test_ut04_control_chars_replaced():
"""Управляющие символы (0x00..0x1F, 0x7F) → '_'."""
name = "abc\x00\x01\x1fdef\x7f"
ascii_fb, _ = safe_filename(name, 1)
assert "\x00" not in ascii_fb
assert "\x1f" not in ascii_fb
assert "\x7f" not in ascii_fb
assert "abc" in ascii_fb and "def" in ascii_fb
def test_ut04_ascii_clean_kept_as_is():
"""ASCII-чистое имя сохраняется в ascii-fallback."""
name = "OSM Trail 42"
ascii_fb, utf8_q = safe_filename(name, 1)
assert ascii_fb == "OSM Trail 42"
assert urllib.parse.unquote(utf8_q) == "OSM Trail 42"