566 lines
23 KiB
YAML
566 lines
23 KiB
YAML
---
|
||
type: test-plan
|
||
work_item_id: ET-008
|
||
title: "Test Plan: GPS-треки с публичных платформ на карте"
|
||
version: 2
|
||
status: draft
|
||
created_at: 2026-06-01
|
||
updated_at: 2026-06-01
|
||
changelog:
|
||
- "v2 (2026-06-01): полная переработка под BRD/TRZ/AC v2 — серверная агрегация, дедупликация, MVT, фильтры активности/источника. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
|
||
authors:
|
||
- "agent:analyst"
|
||
|
||
test_suites:
|
||
|
||
- name: unit-config-loader
|
||
type: unit
|
||
description: "Загрузка и валидация YAML-конфигов sources/regions"
|
||
cases:
|
||
- id: U-01
|
||
name: "Валидный gps_sources.yaml парсится"
|
||
input: "Корректный YAML с 3 источниками"
|
||
expected: "Возвращает список объектов Source с обязательными полями"
|
||
|
||
- id: U-02
|
||
name: "Источник без license_adr — ошибка"
|
||
input: "YAML с enabled=true, но без license_adr"
|
||
expected: "ConfigError: 'enabled source requires license_adr'"
|
||
|
||
- id: U-03
|
||
name: "Регион с unknown source — ошибка"
|
||
input: "regions.sources содержит ID, которого нет в sources.yaml"
|
||
expected: "ConfigError: 'unknown source id'"
|
||
|
||
- id: U-04
|
||
name: "Bbox региона валидируется"
|
||
input: "bbox=[200, 100, 250, 150]"
|
||
expected: "ConfigError: 'bbox out of valid range'"
|
||
|
||
- id: U-05
|
||
name: "Disabled source игнорируется в pipeline"
|
||
input: "Регион ссылается на disabled source"
|
||
expected: "Pipeline пропускает этот source, warning в логе"
|
||
|
||
- name: unit-dedup
|
||
type: unit
|
||
description: "compute_dedup_key и merge-логика"
|
||
cases:
|
||
- id: U-10
|
||
name: "Два трека с одинаковым bbox+length+date → один ключ"
|
||
input: "geom1, geom2 с близкими bounds, length_m differ < 5%, dates same day"
|
||
expected: "compute_dedup_key(g1) == compute_dedup_key(g2)"
|
||
|
||
- id: U-11
|
||
name: "Разные даты → разные ключи"
|
||
input: "Те же bbox+length, daty отличаются на 2 дня"
|
||
expected: "compute_dedup_key различаются"
|
||
|
||
- id: U-12
|
||
name: "Bbox-округление до 0.01°"
|
||
input: "geom1.bounds=(37.6173, 55.7558, …), geom2.bounds=(37.6171, 55.7559, …)"
|
||
expected: "Один ключ (округление до 2 знаков)"
|
||
|
||
- id: U-13
|
||
name: "Merge: union sources"
|
||
input: "track в БД с sources=['osm'], новый с source='enduro_russia', тот же dedup_key"
|
||
expected: "Запись в БД обновлена: sources=['osm','enduro_russia']"
|
||
|
||
- id: U-14
|
||
name: "Merge: union external_urls"
|
||
input: "track в БД с external_urls=[...A], новый с [...B], тот же dedup_key"
|
||
expected: "В БД external_urls=[...A,...B] без дубликатов"
|
||
|
||
- id: U-15
|
||
name: "Merge: приоритет metadata по порядку sources.yaml"
|
||
input: "OSM (priority 1) собрал name='X', EnduroRussia (priority 2) собрал name='Y' с тем же dedup_key"
|
||
expected: "В БД name='X' (приоритет первого source)"
|
||
|
||
- name: unit-activity-mapping
|
||
type: unit
|
||
description: "Маппинг категорий источников в ACTIVITY_TYPES"
|
||
cases:
|
||
- id: U-20
|
||
name: "OSM tag 'enduro' → 'enduro'"
|
||
input: "['enduro', 'motorcycle']"
|
||
expected: "'enduro'"
|
||
|
||
- id: U-21
|
||
name: "OSM tag 'mtb' → 'bicycle'"
|
||
input: "['mtb']"
|
||
expected: "'bicycle'"
|
||
|
||
- id: U-22
|
||
name: "Unknown tag → 'other'"
|
||
input: "['xyz']"
|
||
expected: "'other'"
|
||
|
||
- id: U-23
|
||
name: "Пустой список тэгов → 'other'"
|
||
input: "[]"
|
||
expected: "'other'"
|
||
|
||
- name: unit-bbox-validation
|
||
type: unit
|
||
description: "Валидация bbox в /api/gps-tracks"
|
||
cases:
|
||
- id: U-30
|
||
name: "Валидный bbox"
|
||
input: "bbox=37.0,55.0,38.0,56.0"
|
||
expected: "validate_bbox() = True"
|
||
|
||
- id: U-31
|
||
name: "bbox out-of-range"
|
||
input: "bbox=200,100,250,150"
|
||
expected: "validate_bbox() = False"
|
||
|
||
- id: U-32
|
||
name: "Перевёрнутый bbox"
|
||
input: "bbox=38,55,37,56 (west > east)"
|
||
expected: "validate_bbox() = False"
|
||
|
||
- id: U-33
|
||
name: "Невалидный формат"
|
||
input: "bbox=foo"
|
||
expected: "validate_bbox() = False"
|
||
|
||
- name: unit-osm-parser
|
||
type: unit
|
||
description: "Парсер OSM trackpoints"
|
||
cases:
|
||
- id: U-40
|
||
name: "Группировка trkpt по gpx_id"
|
||
input: "GPX 1.0 с trkpt разных gpx_id"
|
||
expected: "Возвращает по треку на каждый gpx_id"
|
||
|
||
- id: U-41
|
||
name: "Анонимные точки (без gpx_id) — пропуск"
|
||
input: "GPX с точками без gpx_id"
|
||
expected: "Эти точки не попадают в результат"
|
||
|
||
- id: U-42
|
||
name: "Bbox-разбиение региона"
|
||
input: "region.bbox=(37, 55, 39, 57), cell_size=0.25"
|
||
expected: "len(cells) = 8 * 8 = 64"
|
||
|
||
- id: U-43
|
||
name: "Расчёт length_m через Haversine"
|
||
input: "trkpt: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
|
||
expected: "length_m ≈ 28300 (±500)"
|
||
|
||
- id: U-44
|
||
name: "Защита от XXE"
|
||
input: "GPX с DOCTYPE и внешней entity"
|
||
expected: "defusedxml блокирует, парсер не выполняет загрузку"
|
||
|
||
- id: U-45
|
||
name: "Тэги из GPX → activity_type"
|
||
input: "<tag>enduro</tag><tag>motorcycle</tag>"
|
||
expected: "activity_type='enduro'"
|
||
|
||
- name: unit-mvt-generation
|
||
type: unit
|
||
description: "Генерация MVT-тайлов для gps_tracks"
|
||
cases:
|
||
- id: U-50
|
||
name: "Тайл z=10 с 50 треками"
|
||
input: "tile_to_bbox(10, x, y), 50 треков в bbox"
|
||
expected: "Валидный MVT с layer gps_tracks, 50 features"
|
||
|
||
- id: U-51
|
||
name: "Упрощение геометрии на z=7"
|
||
input: "Трек 1000 точек, z=7"
|
||
expected: "После simplify_coords ≤ 100 точек"
|
||
|
||
- id: U-52
|
||
name: "Min-length фильтр на z ≤ 7"
|
||
input: "Треки с length_m=500 и 5000 на z=7"
|
||
expected: "Только трек ≥ 2000м попадает в тайл (min_length для z≤7)"
|
||
|
||
- id: U-53
|
||
name: "Properties в feature"
|
||
input: "Track в БД"
|
||
expected: "feature.properties содержит id, activity, source, sources,
|
||
length_km, name, ext_url"
|
||
|
||
- id: U-54
|
||
name: "Пустой тайл"
|
||
input: "Bbox без треков"
|
||
expected: "build_mvt() возвращает b'' (или валидный пустой MVT)"
|
||
|
||
- name: unit-color-palette
|
||
type: unit
|
||
description: "Цветовая палитра по источнику и активности"
|
||
cases:
|
||
- id: U-60
|
||
name: "Color by source: OSM = #3cb44b"
|
||
input: "feature.source='osm'"
|
||
expected: "Match-expression возвращает '#3cb44b'"
|
||
|
||
- id: U-61
|
||
name: "Color by activity: enduro = #e6194b"
|
||
input: "feature.activity='enduro'"
|
||
expected: "'#e6194b'"
|
||
|
||
- id: U-62
|
||
name: "Unknown source → fallback"
|
||
input: "feature.source='unknown'"
|
||
expected: "'#808080' (или fallback из палитры)"
|
||
|
||
- name: integration-pipeline
|
||
type: integration
|
||
description: "Pipeline gps_collect.py end-to-end с mock-источниками"
|
||
cases:
|
||
- id: I-01
|
||
name: "Полный прогон с 1 mock-источником"
|
||
input: "Mock OSM API → 100 треков; пустая БД"
|
||
expected: "После прогона в БД 100 tracks, pipeline_runs.status='ok',
|
||
tracks_new=100, tracks_updated=0"
|
||
|
||
- id: I-02
|
||
name: "Повторный прогон того же источника — все треки updated"
|
||
input: "Тот же mock + та же БД с предыдущей записью"
|
||
expected: "tracks_new=0, tracks_updated=100"
|
||
|
||
- id: I-03
|
||
name: "Прогон двух источников с пересечением"
|
||
input: "OSM mock = 100 треков, EnduroRussia mock = 50, из них 20 — те же по dedup_key"
|
||
expected: "В БД 130 уникальных записей (100 + 50 - 20). 20 пересекающихся имеют sources=['osm','enduro_russia']"
|
||
|
||
- id: I-04
|
||
name: "Падение одного источника"
|
||
input: "OSM mock OK, EnduroRussia mock возвращает 503"
|
||
expected: "OSM треки в БД, EnduroRussia status='error' в pipeline_runs,
|
||
но pipeline exit=0 (не strict-mode)"
|
||
|
||
- id: I-05
|
||
name: "Dry-run"
|
||
input: "Любой источник + флаг --dry-run"
|
||
expected: "БД не меняется, pipeline_runs не пишется,
|
||
stdout содержит план"
|
||
|
||
- id: I-06
|
||
name: "Rate-limit соблюдается"
|
||
input: "Mock source с rate_limit_sec=2, 5 запросов"
|
||
expected: "Суммарное время ≥ 8 сек (4 интервала × 2 сек)"
|
||
|
||
- id: I-07
|
||
name: "Backoff на 429"
|
||
input: "Mock source первый раз 429, второй раз 200"
|
||
expected: "Pipeline делает retry после exponential backoff,
|
||
трек собран"
|
||
|
||
- name: integration-endpoint-geojson
|
||
type: integration
|
||
description: "/api/gps-tracks GeoJSON"
|
||
cases:
|
||
- id: I-20
|
||
name: "Малый bbox с фильтрами"
|
||
input: "GET /api/gps-tracks?bbox=...&activity=enduro&source=osm"
|
||
expected: "200, FeatureCollection только enduro+OSM треков"
|
||
|
||
- id: I-21
|
||
name: "Truncation"
|
||
input: "В bbox 1500 треков, limit=500"
|
||
expected: "returned=500, total_in_bbox=1500, truncated=true"
|
||
|
||
- id: I-22
|
||
name: "Невалидный bbox → 400"
|
||
input: "bbox=foo"
|
||
expected: "400, JSON error"
|
||
|
||
- id: I-23
|
||
name: "Bbox в океане → пустой результат"
|
||
input: "bbox=0,0,1,1"
|
||
expected: "200, features=[], total=0"
|
||
|
||
- id: I-24
|
||
name: "CORS headers"
|
||
input: "Origin: https://example.com"
|
||
expected: "Response содержит Access-Control-Allow-Origin: *"
|
||
|
||
- id: I-25
|
||
name: "Производительность"
|
||
input: "100 запросов на bbox с 500 треков"
|
||
expected: "p95 ≤ 300 мс"
|
||
|
||
- name: integration-endpoint-mvt
|
||
type: integration
|
||
description: "/api/gps-tracks/tiles/{z}/{x}/{y}.mvt"
|
||
cases:
|
||
- id: I-30
|
||
name: "Тайл MVT отдаётся"
|
||
input: "GET /api/gps-tracks/tiles/10/623/325.mvt"
|
||
expected: "200, Content-Type: application/x-protobuf,
|
||
X-Cache: MISS"
|
||
|
||
- id: I-31
|
||
name: "Cache hit"
|
||
input: "Повторный запрос того же тайла"
|
||
expected: "X-Cache: HIT, ≤ 20 мс"
|
||
|
||
- id: I-32
|
||
name: "Невалидные z/x/y"
|
||
input: "z=25 / x вне диапазона"
|
||
expected: "400"
|
||
|
||
- id: I-33
|
||
name: "Очистка кэша"
|
||
input: "POST /api/gps-tracks/cache/clear, повторный запрос тайла"
|
||
expected: "X-Cache: MISS"
|
||
|
||
- name: integration-endpoint-health
|
||
type: integration
|
||
description: "/api/gps-tracks/health"
|
||
cases:
|
||
- id: I-40
|
||
name: "Полный отчёт"
|
||
input: "GET /api/gps-tracks/health"
|
||
expected: "200, JSON со всеми полями (см. REQ-F-12)"
|
||
|
||
- id: I-41
|
||
name: "БД отсутствует"
|
||
input: "Удалить data/gps_tracks.sqlite, GET /api/gps-tracks/health"
|
||
expected: "503 или 200 с tracks_total=0 и warning"
|
||
|
||
- id: I-42
|
||
name: "Счётчики корректны"
|
||
input: "БД с 100 OSM + 50 EnduroRussia"
|
||
expected: "tracks_by_source: {osm: 100, enduro_russia: 50}"
|
||
|
||
- name: integration-web-layer
|
||
type: integration
|
||
description: "Клиентский слой публичных треков"
|
||
cases:
|
||
- id: I-50
|
||
name: "Включение/выключение слоя"
|
||
input: "Симуляция click на #public-tracks-cb"
|
||
expected: "map.getSource('gps-tracks-tiles') существует,
|
||
layer 'gps-tracks-layer' visibility=visible"
|
||
|
||
- id: I-51
|
||
name: "Фильтр по активности через setFilter"
|
||
input: "filters.activities = ['enduro']"
|
||
expected: "map.getFilter('gps-tracks-layer') содержит ['in', ['get','activity'], ['literal',['enduro']]]"
|
||
|
||
- id: I-52
|
||
name: "Переключение color-mode"
|
||
input: "Переключить с source на activity"
|
||
expected: "Layer paint['line-color'] переустановлен на activity-палитру"
|
||
|
||
- id: I-53
|
||
name: "GeoJSON-загрузка при z ≥ 12"
|
||
input: "map.zoom=14, moveend"
|
||
expected: "Через 500мс debounce — fetch /api/gps-tracks?bbox=…"
|
||
|
||
- id: I-54
|
||
name: "AbortController при быстром pan"
|
||
input: "Два moveend подряд за 100мс"
|
||
expected: "Первый fetch отменён, выполняется только второй"
|
||
|
||
- id: I-55
|
||
name: "Halo на спутнике"
|
||
input: "applyBaseLayer('satellite'), public-tracks включен"
|
||
expected: "layer 'gps-tracks-halo-satellite' visibility=visible"
|
||
|
||
- id: I-56
|
||
name: "Halo выключен на схеме"
|
||
input: "applyBaseLayer('schematic')"
|
||
expected: "halo visibility=none"
|
||
|
||
- id: I-57
|
||
name: "Сохранение слоя при setStyle"
|
||
input: "Переключение тёмной темы (switchMapStyle)"
|
||
expected: "rebuildMapOverlays() → restorePublicTracksState() →
|
||
слой пересоздан, фильтры применены"
|
||
|
||
- name: e2e-pipeline
|
||
type: e2e
|
||
description: "Полный pipeline на тестовых mock-источниках"
|
||
cases:
|
||
- id: E-01
|
||
name: "Сбор → API → визуализация"
|
||
steps:
|
||
- "Очистить test-БД"
|
||
- "Запустить pipeline с mock OSM + mock EnduroRussia"
|
||
- "Проверить: tracks_total > 0 в /api/gps-tracks/health"
|
||
- "Открыть веб-интерфейс"
|
||
- "Включить чекбокс «Публичные треки»"
|
||
- "Убедиться: на карте видны линии треков"
|
||
- "Кликнуть по треку → popup с метаданными"
|
||
|
||
- id: E-02
|
||
name: "Дедупликация — два прогона"
|
||
steps:
|
||
- "Запустить pipeline (mock-источники отдают 100 треков)"
|
||
- "Запомнить tracks_total"
|
||
- "Запустить pipeline повторно (mock отдаёт те же 100)"
|
||
- "Убедиться: tracks_total не изменился"
|
||
- "Убедиться: pipeline_runs.tracks_updated=100"
|
||
|
||
- name: e2e-ui-filters
|
||
type: e2e
|
||
description: "UI-фильтры по активности и источнику"
|
||
cases:
|
||
- id: E-10
|
||
name: "Открытие фильтров и переключение"
|
||
steps:
|
||
- "Включить чекбокс «Публичные треки»"
|
||
- "Нажать «Фильтры…» → открывается #sheet-gps-filters"
|
||
- "Снять все галки активности кроме «Эндуро»"
|
||
- "Убедиться: на карте видны только enduro-треки"
|
||
- "Снять «OSM» в источниках"
|
||
- "Убедиться: OSM enduro-треки скрылись"
|
||
|
||
- id: E-11
|
||
name: "Переключение color-mode"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Открыть фильтры"
|
||
- "Выбрать «По активности»"
|
||
- "Убедиться: цвета линий перерисованы (например, enduro = красный)"
|
||
- "Перезагрузить страницу"
|
||
- "Убедиться: color-mode='activity' сохранён"
|
||
|
||
- id: E-12
|
||
name: "Persistence фильтров"
|
||
steps:
|
||
- "Настроить фильтры (только moto, только EnduroRussia)"
|
||
- "Перезагрузить страницу"
|
||
- "Открыть фильтры"
|
||
- "Убедиться: чекбоксы соответствуют настройкам"
|
||
|
||
- name: e2e-popup
|
||
type: e2e
|
||
description: "Popup трека"
|
||
cases:
|
||
- id: E-20
|
||
name: "Popup полный набор полей"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Кликнуть на трек на карте"
|
||
- "Убедиться: popup содержит name, activity-иконку, км, дату, user, sources"
|
||
- "Кликнуть по ссылке источника"
|
||
- "Убедиться: открыта новая вкладка"
|
||
|
||
- id: E-21
|
||
name: "Popup для трека без user"
|
||
steps:
|
||
- "Найти трек без user"
|
||
- "Кликнуть → popup без строки «Автор»"
|
||
|
||
- name: e2e-compat
|
||
type: e2e
|
||
description: "Совместимость с другими функциями"
|
||
cases:
|
||
- id: E-30
|
||
name: "Слой + спутник + halo"
|
||
steps:
|
||
- "Включить «Публичные треки»"
|
||
- "Переключить подложку на «Спутник»"
|
||
- "Убедиться: треки видны на спутнике с белой обводкой"
|
||
|
||
- id: E-31
|
||
name: "Слой + тёмная тема"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Переключить тёмную тему"
|
||
- "Убедиться: треки остаются на карте"
|
||
- "Убедиться: фильтры сохранены"
|
||
|
||
- id: E-32
|
||
name: "Слой + личный GPX (ET-006)"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Загрузить личный GPX"
|
||
- "Убедиться: оба видны"
|
||
- "Убедиться: личный трек выше публичных по z-order"
|
||
|
||
- id: E-33
|
||
name: "Слой + маршрут OSRM"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Построить маршрут OSRM"
|
||
- "Убедиться: маршрут OSRM визуально выше публичных треков"
|
||
|
||
- id: E-34
|
||
name: "Слой + hillshade"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Включить hillshade"
|
||
- "Убедиться: оба видны"
|
||
|
||
- name: e2e-low-zoom-protection
|
||
type: e2e
|
||
description: "Защита от шторма запросов на low-zoom"
|
||
cases:
|
||
- id: E-40
|
||
name: "Слой скрыт на z<8"
|
||
steps:
|
||
- "Включить слой"
|
||
- "Отзумиться до z=5"
|
||
- "Убедиться: линии не отображаются"
|
||
- "Убедиться: появилась подсказка «Зум 8+» у чекбокса"
|
||
|
||
- id: E-41
|
||
name: "Pan на z 14 не штормит запросы"
|
||
steps:
|
||
- "Включить слой, z=14"
|
||
- "Быстро панить карту (5 раз за 1 сек)"
|
||
- "Проверить network log: не более 2 запросов /api/gps-tracks"
|
||
|
||
- name: load-pipeline
|
||
type: load
|
||
description: "Нагрузочные сценарии pipeline и API"
|
||
cases:
|
||
- id: L-01
|
||
name: "Полный прогон pipeline на ЦФО+Чувашию (mock)"
|
||
input: "Mock OSM с реальным объёмом ≈ 50K треков"
|
||
expected: "Прогон завершается за ≤ 6 часов (cron-окно)"
|
||
|
||
- id: L-02
|
||
name: "API под нагрузкой"
|
||
input: "10 параллельных клиентов делают по 100 запросов /api/gps-tracks"
|
||
expected: "p95 ≤ 500 мс, нет ошибок"
|
||
|
||
- id: L-03
|
||
name: "MVT-тайлы под нагрузкой"
|
||
input: "100 параллельных запросов разных тайлов"
|
||
expected: "p95 cold ≤ 300 мс, hit-rate кэша > 80% на повторах"
|
||
|
||
test_data:
|
||
fixtures_dir: "tests/fixtures/gps-tracks/"
|
||
fixtures:
|
||
- name: "osm-trackpoints-bbox-moscow.gpx"
|
||
description: "Реальный ответ OSM API на bbox центра Москвы"
|
||
- name: "osm-trackpoints-multipage.json"
|
||
description: "Серия ответов OSM с has_more=true на нескольких страницах"
|
||
- name: "enduro-russia-mock-listing.html"
|
||
description: "Главная страница региона на EnduroRussia (mock)"
|
||
- name: "enduro-russia-mock-track.gpx"
|
||
description: "GPX-файл, отдаваемый EnduroRussia mock"
|
||
- name: "ttrails-mock-track.gpx"
|
||
description: "GPX от ttrails mock"
|
||
- name: "xxe-payload.gpx"
|
||
description: "GPX с DOCTYPE и внешней entity (для проверки defusedxml)"
|
||
- name: "dedup-pair-osm-enduro.json"
|
||
description: "Пара треков (одна и та же поездка из двух источников) для проверки dedup"
|
||
- name: "gps_tracks_seed.sql"
|
||
description: "SQL-сид: 1000 синтетических треков для интеграционных тестов"
|
||
|
||
test_environment:
|
||
mock_servers:
|
||
- "Mock OSM API (отвечает на /api/0.6/trackpoints и /api/0.6/gpx/<id>)"
|
||
- "Mock EnduroRussia.ru (HTML-страницы + GPX-файлы)"
|
||
- "Mock ttrails.ru"
|
||
cron_simulation:
|
||
- "В тестах cron заменяется на pytest fixture, вызывающий run() напрямую"
|
||
db_isolation:
|
||
- "Каждый тест использует in-memory или временный sqlite-файл в pytest tmp_path"
|
||
network:
|
||
- "Все исходящие HTTP в unit/integration — через httpx_mock или respx (без реальной сети)"
|
||
notes:
|
||
- "L-01 (полный прогон pipeline) запускается отдельно, не в обычном CI"
|
||
- "E2E UI-тесты — Playwright; URL test-среды https://openclaw.mva154.duckdns.org/enduro/ (см. 04b-ui-test-cases.md)"
|
||
- "Для load-тестов использовать pytest-benchmark + locust"
|