251 lines
11 KiB
YAML
251 lines
11 KiB
YAML
work_item: ET-011
|
||
title: Скачивание трека из popup на карте
|
||
version: 1
|
||
generated_by: analyst
|
||
|
||
# Категории тестов:
|
||
# - unit — изолированные функции (сборщик GPX, санитизатор имени)
|
||
# - integration — FastAPI endpoint через TestClient
|
||
# - e2e — Playwright, end-to-end в браузере
|
||
# UI-кейсы для визуальной/интерактивной проверки — см. 04b-ui-test-cases.md
|
||
|
||
tests:
|
||
|
||
# ─── UNIT ─────────────────────────────────────────────────────
|
||
|
||
- id: UT-01
|
||
type: unit
|
||
name: build_gpx — корректная структура GPX 1.1
|
||
file: tests/api/test_gps_tracks_gpx_builder.py
|
||
covers: [REQ-F-03, REQ-NF-03]
|
||
steps:
|
||
- Подать на вход искусственный трек (5 точек, name, description, activity_type="enduro", sources=["osm"], external_urls=["https://www.openstreetmap.org/way/1"]).
|
||
- Получить строку GPX.
|
||
- Распарсить через ElementTree.
|
||
assertions:
|
||
- root.tag == "{http://www.topografix.com/GPX/1/1}gpx"
|
||
- root.attrib["version"] == "1.1"
|
||
- root.attrib["creator"] == "Enduro Trails"
|
||
- в metadata присутствует <name> с переданным именем
|
||
- в metadata присутствует <link href="https://www.openstreetmap.org/way/1">
|
||
- ровно один <trk>, ровно один <trkseg>
|
||
- число <trkpt> == 5
|
||
- у trkpt атрибуты lat и lon — float
|
||
|
||
- id: UT-02
|
||
type: unit
|
||
name: build_gpx — пустые/NULL поля
|
||
file: tests/api/test_gps_tracks_gpx_builder.py
|
||
covers: [REQ-F-03]
|
||
steps:
|
||
- Трек с name=None, description=None, created_at=None, user=None, external_urls=[].
|
||
assertions:
|
||
- GPX валиден (по схеме)
|
||
- <name> = "Без названия" или его аналог
|
||
- элементы <desc>, <time>, <author>, <link> отсутствуют (а не пустые)
|
||
|
||
- id: UT-03
|
||
type: unit
|
||
name: build_gpx — соответствие схеме XSD
|
||
file: tests/api/test_gps_tracks_gpx_builder.py
|
||
covers: [REQ-NF-03]
|
||
steps:
|
||
- Сгенерировать GPX из 3 разных треков (минимальный, типичный, с UTF-8).
|
||
- Валидировать каждый через lxml.etree.XMLSchema (gpx.xsd закоммитить в tests/fixtures/).
|
||
assertions:
|
||
- schema.validate(tree) == True для всех 3 случаев
|
||
|
||
- id: UT-04
|
||
type: unit
|
||
name: safe_filename — санитизация
|
||
file: tests/api/test_gps_tracks_filename.py
|
||
covers: [REQ-F-04]
|
||
cases:
|
||
- input: "По грязи к Чёрному озеру"
|
||
expected_ascii_fallback: содержит только ASCII, длина ≤ 80
|
||
expected_utf8: percent-encoded UTF-8 строка
|
||
- input: "Trail/with:bad*chars?"
|
||
expected_ascii: подчёркивания вместо запрещённых символов
|
||
- input: ""
|
||
track_id: 42
|
||
expected: "track-42"
|
||
- input: "X" * 200
|
||
expected_length: ≤ 80
|
||
|
||
- id: UT-05
|
||
type: unit
|
||
name: wkb_to_coords — повторное использование существующего парсера
|
||
file: tests/api/test_gps_tracks_gpx_builder.py
|
||
covers: [REQ-F-03]
|
||
note: уже покрыто косвенно в ET-008, но добавить smoke-проверку на пограничный случай (2 точки).
|
||
|
||
# ─── INTEGRATION ───────────────────────────────────────────────
|
||
|
||
- id: IT-01
|
||
type: integration
|
||
name: GET /api/gps-tracks/{id}/download — happy path
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-F-02, AC-3]
|
||
steps:
|
||
- Инициализировать тестовую БД с одним треком (id=1, geom=LineString из 10 точек).
|
||
- GET /api/gps-tracks/1/download через TestClient.
|
||
assertions:
|
||
- status_code == 200
|
||
- response.headers["content-type"] == "application/gpx+xml"
|
||
- "attachment" in response.headers["content-disposition"]
|
||
- "filename*=UTF-8''" in response.headers["content-disposition"]
|
||
- response.text.startswith("<?xml")
|
||
- "<gpx" in response.text and 'version="1.1"' in response.text
|
||
- response.text.count("<trkpt") == 10
|
||
|
||
- id: IT-02
|
||
type: integration
|
||
name: GET /api/gps-tracks/{id}/download — 404 для несуществующего id
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-F-02, AC-7]
|
||
steps:
|
||
- GET /api/gps-tracks/99999999/download
|
||
assertions:
|
||
- status_code == 404
|
||
- response.json()["detail"] упоминает не_найден / not_found / track_not_found
|
||
|
||
- id: IT-03
|
||
type: integration
|
||
name: GET /api/gps-tracks/{id}/download — невалидный format
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-F-02, AC-8]
|
||
steps:
|
||
- GET /api/gps-tracks/1/download?format=fit
|
||
assertions:
|
||
- status_code == 400
|
||
|
||
- id: IT-04
|
||
type: integration
|
||
name: Patho-трек > 200k точек → 413
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-NF-02, AC-9]
|
||
steps:
|
||
- Подложить в БД запись с points_count=300000 (можно фиктивную, geom не нужен — проверка идёт по points_count до сборки).
|
||
- GET /api/gps-tracks/<id>/download
|
||
assertions:
|
||
- status_code == 413
|
||
|
||
- id: IT-05
|
||
type: integration
|
||
name: Лицензионный фильтр — 403 для запрещённого источника (Q-1 conditional)
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-F-06, AC-11]
|
||
enabled_if: "Owner закрыл Q-1 как 'только OSM'"
|
||
steps:
|
||
- Трек с sources=["wikiloc"], external_urls=["https://wikiloc.com/..."]
|
||
- GET /api/gps-tracks/<id>/download
|
||
assertions:
|
||
- status_code == 403
|
||
- response.json()["external_urls"] == ["https://wikiloc.com/..."]
|
||
|
||
- id: IT-06
|
||
type: integration
|
||
name: UTF-8 имя файла в Content-Disposition
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-F-04, REQ-NF-05, AC-4]
|
||
steps:
|
||
- Трек с name="По грязи к Чёрному озеру"
|
||
- GET .../download
|
||
assertions:
|
||
- "filename*=UTF-8''" в Content-Disposition
|
||
- decoded UTF-8 имя == "По грязи к Чёрному озеру.gpx"
|
||
- "filename=" (без звёздочки) — ASCII-fallback, без кириллицы
|
||
|
||
- id: IT-07
|
||
type: integration
|
||
name: Валидация GPX-ответа по XSD
|
||
file: tests/api/test_gps_tracks_download.py
|
||
covers: [REQ-NF-03, AC-5]
|
||
steps:
|
||
- Скачать GPX через TestClient.
|
||
- Валидировать ответ через lxml.etree.XMLSchema по gpx.xsd.
|
||
assertions:
|
||
- validation passes без warnings/errors
|
||
|
||
- id: IT-08
|
||
type: integration
|
||
name: Регрессия — существующие GPS-эндпоинты живы
|
||
file: tests/api/test_gps_tracks_endpoint.py
|
||
covers: [AC-15]
|
||
note: smoke-проверка, что добавление нового route не сломало GET /api/gps-tracks, /tiles/..., /health.
|
||
|
||
# ─── E2E (Playwright, mounted browser) ─────────────────────────
|
||
|
||
- id: E2E-01
|
||
type: e2e
|
||
name: Тап трека → popup → клик «Скачать» → файл в загрузках (desktop)
|
||
file: tests/web/test_track_download.spec.ts
|
||
covers: [REQ-F-01, REQ-F-05, AC-1, AC-2]
|
||
viewport: desktop
|
||
steps:
|
||
- Открыть https://openclaw.mva154.duckdns.org/enduro/
|
||
- Включить слой «Публичные треки» (раскрыть terrain-popup, поставить #public-tracks-cb).
|
||
- Дождаться загрузки тайлов (~5000ms).
|
||
- Кликнуть в координату с известным треком (либо использовать map.queryRenderedFeatures + map.click).
|
||
- Дождаться появления popup (.maplibregl-popup .track-popup).
|
||
- Ожидать кнопку с aria-label="Скачать GPX" внутри popup.
|
||
- Кликнуть на кнопку и перехватить событие download через context.waitForEvent('download').
|
||
assertions:
|
||
- download.suggestedFilename().endsWith('.gpx')
|
||
- размер файла > 100 байт
|
||
- первые 100 байт содержат "<?xml" и "<gpx"
|
||
|
||
- id: E2E-02
|
||
type: e2e
|
||
name: Mobile — popup и кнопка видны
|
||
file: tests/web/test_track_download.spec.ts
|
||
covers: [REQ-NF-04, AC-13]
|
||
viewport: mobile (375x667)
|
||
steps:
|
||
- см. E2E-01, но с deviceScaleFactor=2, isMobile=true.
|
||
assertions:
|
||
- кнопка «Скачать» видима (visible) и имеет bounding box ≥ 32×32 px
|
||
- popup не выходит за пределы viewport
|
||
|
||
- id: E2E-03
|
||
type: e2e
|
||
name: Ошибка 404 — toast пользователю
|
||
file: tests/web/test_track_download.spec.ts
|
||
covers: [REQ-F-05, AC-7]
|
||
steps:
|
||
- Замокать ответ /api/gps-tracks/*/download через page.route() — вернуть 404.
|
||
- Триггернуть download.
|
||
assertions:
|
||
- появляется #app-toast с текстом «Трек не найден» (либо аналог)
|
||
|
||
- id: E2E-04
|
||
type: e2e
|
||
name: Лицензионный фильтр — toast «Источник запрещает» (conditional)
|
||
file: tests/web/test_track_download.spec.ts
|
||
covers: [REQ-F-06, AC-11]
|
||
enabled_if: "Owner закрыл Q-1 как 'только OSM'"
|
||
steps:
|
||
- Замокать ответ /api/gps-tracks/*/download → 403 с body {"detail":"source_forbidden","external_urls":["https://wikiloc.com/x"]}.
|
||
assertions:
|
||
- toast содержит текст про «источник»
|
||
- есть кликабельная ссылка / кнопка на wikiloc URL
|
||
|
||
# ─── Покрытие AC ────────────────────────────────────────────────
|
||
|
||
coverage_matrix:
|
||
AC-1: [E2E-01, E2E-02]
|
||
AC-2: [E2E-01]
|
||
AC-3: [IT-01]
|
||
AC-4: [UT-04, IT-06]
|
||
AC-5: [UT-03, IT-07]
|
||
AC-6: ['manual smoke (см. acceptance §AC-6)']
|
||
AC-7: [IT-02, E2E-03]
|
||
AC-8: [IT-03]
|
||
AC-9: [IT-04]
|
||
AC-10: [UT-01]
|
||
AC-11: [IT-05, E2E-04]
|
||
AC-12: ['manual perf check, не блокирует merge']
|
||
AC-13: [E2E-02]
|
||
AC-14: ['покрывается визуально через UI test cases 04b']
|
||
AC-15: [IT-08]
|