Files
enduro-trails/docs/work-items/ET-011/04-test-plan.yaml
claude-bot 2bf08a10e3
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
analyst(ET): auto-commit from analyst run_id=63
2026-06-03 20:05:12 +00:00

251 lines
11 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.
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]