"""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-'.""" 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"