425 lines
18 KiB
YAML
425 lines
18 KiB
YAML
---
|
||
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"
|