Files
enduro-trails/docs/work-items/ET-008/04-test-plan.yaml

425 lines
18 KiB
YAML
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.
---
type: test-plan
work_item_id: ET-008
title: "Test Plan: GPS-треки с публичных платформ на карте"
version: 1
status: draft
created_at: 2026-06-01
updated_at: 2026-06-01
authors:
- "agent:analyst"
test_suites:
- name: unit-gpx-proxy-validation
type: unit
description: "SSRF-валидация URL в gpx_proxy.is_safe_url()"
cases:
- id: U-01
name: "Принимает валидный публичный HTTPS URL"
input: "https://example.com/track.gpx (резолвится в публичный IP)"
expected: "is_safe_url() возвращает True"
- id: U-02
name: "Отклоняет схему ftp://"
input: "ftp://example.com/track.gpx"
expected: "is_safe_url() возвращает False"
- id: U-03
name: "Отклоняет схему file://"
input: "file:///etc/passwd"
expected: "is_safe_url() возвращает False"
- id: U-04
name: "Отклоняет loopback IP"
input: "http://127.0.0.1/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-05
name: "Отклоняет приватный IP (10.0.0.0/8)"
input: "http://10.1.2.3/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-06
name: "Отклоняет приватный IP (192.168.0.0/16)"
input: "http://192.168.1.1/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-07
name: "Отклоняет приватный IP (172.16.0.0/12)"
input: "http://172.16.0.1/x.gpx"
expected: "is_safe_url() возвращает False"
- id: U-08
name: "Отклоняет link-local IP (169.254.x.x)"
input: "http://169.254.169.254/metadata"
expected: "is_safe_url() возвращает False"
- id: U-09
name: "Отклоняет невалидный URL"
input: "not a url"
expected: "is_safe_url() возвращает False (без exception)"
- id: U-10
name: "Отклоняет хост, который не резолвится"
input: "http://nonexistent-host-xyz-12345.invalid/x.gpx"
expected: "is_safe_url() возвращает False"
- name: unit-bbox-validation
type: unit
description: "Валидация bbox в osm_traces"
cases:
- id: U-20
name: "Принимает малый bbox"
input: "bbox=[37.6, 55.7, 37.7, 55.8] (0.01 deg²)"
expected: "validate_bbox() возвращает True"
- id: U-21
name: "Отклоняет bbox > 0.25 deg²"
input: "bbox=[37.0, 55.0, 38.0, 56.0] (1.0 deg²)"
expected: "validate_bbox() возвращает False"
- id: U-22
name: "Отклоняет невалидные координаты"
input: "bbox=[200, 100, 250, 150]"
expected: "validate_bbox() возвращает False"
- id: U-23
name: "Отклоняет перевёрнутый bbox (west > east)"
input: "bbox=[38.0, 55.0, 37.0, 56.0]"
expected: "validate_bbox() возвращает False"
- name: unit-cache
type: unit
description: "LRU кэш с TTL"
cases:
- id: U-30
name: "TTL истёк → cache miss"
input: "Положить запись с TTL 1 сек, ждать 2 сек, запросить"
expected: "Возвращает None (или вызывает loader)"
- id: U-31
name: "LRU вытеснение при переполнении"
input: "Заполнить кэш max=4 записями, добавить 5-ю"
expected: "Первая (LRU) запись вытеснена"
- id: U-32
name: "Округление bbox-ключа до 4 знаков"
input: "bbox=[37.6172999, 55.7558001, ...] и bbox=[37.6173, 55.7558, ...]"
expected: "Один и тот же кэш-ключ → cache hit"
- id: U-33
name: "URL > 5 МБ не кэшируется"
input: "Положить запись размером 6 МБ"
expected: "Запись не попадает в кэш (cache.get → None)"
- name: unit-osm-parser
type: unit
description: "Парсинг OSM trackpoints GPX → JSON"
cases:
- id: U-40
name: "Извлечение точек из GPX 1.0"
input: "GPX с 1 <trk>, 1 <trkseg>, 50 <trkpt>"
expected: "JSON: {tracks: [{points_count: 50, distance_km: ~X}]}"
- id: U-41
name: "Расчёт длины через Haversine"
input: "GPX с 3 точками: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
expected: "distance_km ≈ 28.3 (±0.5)"
- id: U-42
name: "Пустой GPX (нет trkpt)"
input: "GPX без точек"
expected: "JSON: {tracks: [], total_points: 0}"
- id: U-43
name: "Защита от XXE (defusedxml)"
input: "GPX с DOCTYPE и внешней entity"
expected: "Парсер не выполняет загрузку внешней entity (или бросает ошибку)"
- name: unit-web-gpx-source
type: unit
description: "Расширение модели window.gpxTracks полем source"
cases:
- id: U-50
name: "Импорт по URL: source.kind='url'"
input: "importGpxFromUrl('https://github.com/x/y.gpx', mockedFetch)"
expected: "Трек добавлен с source={kind:'url', url:'https://github.com/x/y.gpx'}"
- id: U-51
name: "Импорт OSM: source.kind='osm'"
input: "importOsmTrace({osm_page:0, osm_bbox:[...], gpx_url:'...'}, mockedFetch)"
expected: "Трек добавлен с source={kind:'osm', osm_page:0, osm_bbox:[...], url:'...'}"
- id: U-52
name: "Обратная совместимость: трек без source читается как 'file'"
input: "window.gpxTracks[0] без поля source"
expected: "renderSourceRow() возвращает '📁 локальный файл'"
- id: U-53
name: "Hostname extraction для URL-источника"
input: "source.url='https://raw.githubusercontent.com/user/repo/main/track.gpx'"
expected: "renderSourceRow() возвращает '🔗 raw.githubusercontent.com'"
- name: integration-gpx-fetch
type: integration
description: "GET /api/gpx/fetch — прокси с реальным HTTP"
cases:
- id: I-01
name: "Успешная загрузка GPX по URL (mock-сервер)"
input: "GET /api/gpx/fetch?url=http://test-server/track.gpx"
expected: "200, Content-Type: application/gpx+xml, тело = GPX, X-Cache: MISS"
- id: I-02
name: "Повторный запрос — cache hit"
input: "GET тот же URL"
expected: "200, X-Cache: HIT, время ≤ 50 мс"
- id: I-03
name: "Отклонение приватного IP"
input: "GET /api/gpx/fetch?url=http://127.0.0.1/x.gpx"
expected: "400, JSON {error: ...}"
- id: I-04
name: "Отклонение редиректа на приватный IP"
input: "Внешний URL → 302 на http://127.0.0.1/x.gpx"
expected: "400, JSON {error: ...}"
- id: I-05
name: "Внешний 404"
input: "URL ведёт на несуществующий путь"
expected: "404, JSON {error: ...}"
- id: I-06
name: "Лимит размера 50 МБ"
input: "Mock-сервер стримит 60 МБ"
expected: "413, соединение прервано до конца"
- id: I-07
name: "Таймаут"
input: "Mock-сервер ничего не отвечает"
expected: "504 после 15 сек"
- id: I-08
name: "URL > 5 МБ не попадает в кэш"
input: "Запросить URL с ответом 6 МБ дважды"
expected: "Второй запрос: X-Cache: MISS, внешний запрос повторно выполнен"
- name: integration-osm-traces
type: integration
description: "GET /api/gpx/osm/traces — OSM API клиент"
cases:
- id: I-20
name: "Bbox-запрос с результатами"
input: "GET /api/gpx/osm/traces?bbox=37.6,55.7,37.65,55.75 (mock OSM API)"
expected: "200, JSON с tracks[], каждый имеет points_count, distance_km, gpx_url"
- id: I-21
name: "Bbox > 0.25 deg² → 400"
input: "bbox=37,55,38,56"
expected: "400, error 'bbox too large'"
- id: I-22
name: "OSM API недоступен → 502"
input: "OSM mock возвращает 500"
expected: "502, JSON error"
- id: I-23
name: "Cache hit на повторный bbox"
input: "Тот же bbox дважды"
expected: "Второй запрос: внешний запрос НЕ выполнен, ответ из кэша"
- id: I-24
name: "Пустой bbox → пустой список"
input: "bbox в океане"
expected: "200, tracks=[], has_more=false"
- id: I-25
name: "Пагинация"
input: "page=0 возвращает has_more=true, page=1 возвращает следующие"
expected: "Корректное смещение, оба запроса валидны"
- name: integration-health-metrics
type: integration
description: "Метрики кэшей в /api/health"
cases:
- id: I-30
name: "Health возвращает размеры кэшей"
input: "GET /api/health"
expected: "JSON содержит gpx_fetch_cache_size, gpx_osm_cache_size (числа ≥ 0)"
- id: I-31
name: "Счётчики растут после запросов"
input: "После N успешных fetch и M osm_traces запросов"
expected: "Размеры кэшей отражают добавленные записи"
- name: e2e-url-import
type: e2e
description: "Импорт GPX по ссылке — полный сценарий"
cases:
- id: E-01
name: "URL-импорт валидного трека"
steps:
- "Открыть приложение"
- "Нажать кнопку GPX в нижнем тулбаре"
- "Переключиться на вкладку «По ссылке»"
- "Вставить URL валидного GPX (тестовый mock)"
- "Нажать «Загрузить»"
- "Убедиться: индикатор показан, через ≤ 5 сек трек на карте"
- "Убедиться: в #gpx-list появилась карточка с источником «🔗 host»"
- "Кликнуть на трек → отображается статистика и профиль высот"
- id: E-02
name: "URL-импорт по Enter"
steps:
- "Активировать «По ссылке»"
- "Вставить URL, нажать Enter"
- "Убедиться: трек загружен (как при клике)"
- id: E-03
name: "Невалидный URL → toast"
steps:
- "Вставить ftp://x.com/y"
- "Нажать «Загрузить»"
- "Убедиться: toast «Невалидная ссылка»"
- "Убедиться: на карте ничего нового"
- id: E-04
name: "Приватный IP блокируется"
steps:
- "Вставить http://192.168.1.1/x.gpx"
- "Нажать «Загрузить»"
- "Убедиться: toast «Эта ссылка недоступна»"
- id: E-05
name: "Не GPX по ссылке"
steps:
- "Вставить URL HTML-страницы"
- "Нажать «Загрузить»"
- "Убедиться: toast «По этой ссылке не GPX-файл»"
- name: e2e-osm-search
type: e2e
description: "Поиск и импорт OSM треков"
cases:
- id: E-10
name: "Поиск треков в области и импорт"
steps:
- "Открыть приложение, отзумиться к области Москвы (zoom 12)"
- "Открыть #sheet-gpx, активировать «Найти рядом»"
- "Нажать «Найти треки в этой области карты»"
- "Убедиться: индикатор, потом список карточек"
- "Нажать «Показать» у первой карточки"
- "Убедиться: трек появился на карте, fit bounds"
- "Убедиться: карточка в найденных получила «✓ Загружен»"
- "Убедиться: в #gpx-list появилась карточка с «🌍 OSM #...»"
- id: E-11
name: "Слишком большая область"
steps:
- "Отзумиться на всю Россию"
- "Активировать «Найти рядом»"
- "Нажать «Найти»"
- "Убедиться: toast «Слишком большая область, увеличьте zoom»"
- id: E-12
name: "Пустая область"
steps:
- "Перейти к области без треков (океан)"
- "Активировать «Найти рядом»"
- "Нажать «Найти»"
- "Убедиться: сообщение «В этой области нет публичных GPS-треков»"
- id: E-13
name: "Пагинация"
steps:
- "Найти треки в области с большим количеством"
- "Убедиться: кнопка «Показать ещё» внизу"
- "Нажать «Показать ещё»"
- "Убедиться: список расширился"
- id: E-14
name: "Повторный импорт → toast"
steps:
- "Импортировать трек по «Показать»"
- "Нажать «Показать» у той же карточки ещё раз"
- "Убедиться: toast «Уже загружен»"
- id: E-15
name: "Внешняя ссылка на osm.org"
steps:
- "Найти треки, нажать «↗» у карточки"
- "Убедиться: новая вкладка открыта на openstreetmap.org"
- name: e2e-mixed-sources
type: e2e
description: "Совместимость трёх источников в одной сессии"
cases:
- id: E-20
name: "3 трека разных источников"
steps:
- "Загрузить 1 локальный файл"
- "Загрузить 1 по URL"
- "Загрузить 1 из OSM"
- "Убедиться: 3 карточки в #gpx-list, разные цвета, разные источники"
- "Удалить URL-трек"
- "Убедиться: 2 трека на карте, корректные источники"
- id: E-21
name: "Сохранение при смене темы"
steps:
- "Загрузить 3 трека разных источников"
- "Переключить тёмную тему"
- "Убедиться: все 3 трека на карте"
- "Убедиться: источники в карточках сохранены"
- id: E-22
name: "Сохранение при включении hillshade"
steps:
- "Загрузить 3 трека"
- "Включить hillshade"
- "Убедиться: все 3 трека видны поверх hillshade"
- name: e2e-cache
type: e2e
description: "Поведение кэша через API"
cases:
- id: E-30
name: "Кэш URL-fetch снижает время"
steps:
- "GET /api/gpx/fetch?url=<test-url> — измерить t1"
- "GET /api/gpx/fetch?url=<тот же url> — измерить t2"
- "Убедиться: t2 < 100 мс, заголовок X-Cache: HIT"
- id: E-31
name: "Размеры кэша в health"
steps:
- "Сделать N запросов /api/gpx/fetch"
- "GET /api/health"
- "Убедиться: gpx_fetch_cache_size == N (или min(N, лимит))"
test_data:
- name: "test-track-public.gpx"
description: "Валидный GPX 1.1, 1 МБ, для URL-импорта (mock-сервер)"
- name: "test-track-large.gpx"
description: "GPX 60 МБ — для проверки лимита размера"
- name: "test-osm-trackpoints.gpx"
description: "Реальный ответ OSM trackpoints API (зафиксирован для mock)"
- name: "test-html-page.html"
description: "HTML вместо GPX — для проверки валидации формата"
- name: "test-xxe-payload.gpx"
description: "GPX с DOCTYPE и внешней entity — для проверки defusedxml"
- name: "bbox-moscow-small"
description: "[37.6, 55.7, 37.65, 55.75] — реальная область с публичными треками OSM"
- name: "bbox-too-large"
description: "[37.0, 55.0, 38.0, 56.0] — > 0.25 deg² для проверки 400"
test_environment:
mock_servers:
- "Mock HTTP-сервер для /api/gpx/fetch тестов (отдаёт фиксированные ответы)"
- "Mock OSM API для /api/gpx/osm/traces тестов"
fixtures_dir: "tests/fixtures/gpx-public/"
notes:
- "OSM API в e2e тестах должен мокироваться, чтобы не зависеть от внешней доступности"
- "Для нагрузочных тестов кэша использовать pytest-benchmark"