319 lines
12 KiB
YAML
319 lines
12 KiB
YAML
---
|
||
type: test-plan
|
||
work_item_id: ET-006
|
||
title: "Test Plan: Скачивание трека из popup на карте"
|
||
version: 1
|
||
status: approved
|
||
created_at: 2026-06-03
|
||
updated_at: 2026-06-03
|
||
authors:
|
||
- "agent:analyst"
|
||
---
|
||
|
||
# Test Plan — ET-006: Скачивание трека из popup на карте
|
||
|
||
# Структура: каждый тест-кейс ссылается на AC и на TRZ-требование.
|
||
# UI-тесты (Playwright визуальные) описаны в 04b-ui-test-cases.md.
|
||
|
||
suites:
|
||
|
||
unit:
|
||
framework: pytest
|
||
path: tests/unit/
|
||
|
||
cases:
|
||
|
||
- id: TC-UNIT-01
|
||
title: "gpx_builder.build_gpx — корректный корень и XML 1.0 декларация"
|
||
covers:
|
||
ac: [AC-04]
|
||
req: [REQ-F-04, REQ-F-06, REQ-F-10]
|
||
given: "TrackRow-фикстура с name='X', 3 точки, sources=['osm']"
|
||
when: "build_gpx(track_row) → bytes"
|
||
then:
|
||
- "первая строка начинается с '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'"
|
||
- "корневой элемент <gpx version='1.1' xmlns='http://www.topografix.com/GPX/1/1'"
|
||
- "содержит xsi:schemaLocation на GPX 1.1 XSD"
|
||
- "содержит атрибут creator='enduro-trails (...)'"
|
||
- "XML парсится без ошибок (ET.fromstring)"
|
||
|
||
- id: TC-UNIT-02
|
||
title: "gpx_builder — точки сохраняются с точностью 6 знаков и в исходном порядке"
|
||
covers:
|
||
ac: [AC-05]
|
||
req: [REQ-F-09]
|
||
given: "track с coords=[(37.123456789, 55.987654321), (37.111111, 55.222222), (37.999999, 55.000001)]"
|
||
when: "сериализация в GPX"
|
||
then:
|
||
- "в <trkseg> ровно 3 <trkpt>"
|
||
- "первый <trkpt lat='55.987654' lon='37.123457'/> (округление до 6 знаков)"
|
||
- "порядок элементов совпадает с входным"
|
||
|
||
- id: TC-UNIT-03
|
||
title: "gpx_builder — metadata содержит name/desc/author/time/link"
|
||
covers:
|
||
ac: [AC-06]
|
||
req: [REQ-F-07]
|
||
given: "track с name='Кольцо', activity_type='enduro', user='vasya', created_at='2025-08-14T09:30:00Z', external_urls=['https://example.com/a','https://example.com/b'], length_m=42500, sources=['osm','wikiloc']"
|
||
when: "build_gpx"
|
||
then:
|
||
- "metadata/name == 'Кольцо'"
|
||
- "metadata/desc содержит 'Эндуро' и '42.5' и 'osm' и 'wikiloc'"
|
||
- "metadata/author/name == 'vasya'"
|
||
- "metadata/time == '2025-08-14T09:30:00Z'"
|
||
- "metadata содержит две <link> в порядке external_urls"
|
||
- "trk/name == 'Кольцо', trk/type == 'enduro'"
|
||
|
||
- id: TC-UNIT-04
|
||
title: "filename builder — очистка запрещённых символов"
|
||
covers:
|
||
ac: [AC-07]
|
||
req: [REQ-F-05]
|
||
cases:
|
||
- input: { name: "OSM/Trail*?<2024>", id: 42 }
|
||
expect_ascii_match: "^[A-Za-z0-9._-]+\\.gpx$"
|
||
expect_utf8: "OSM-Trail-2024.gpx"
|
||
- input: { name: "", id: 7 }
|
||
expect_ascii: "track-7.gpx"
|
||
- input: { name: null, id: 7 }
|
||
expect_ascii: "track-7.gpx"
|
||
- input: { name: " ", id: 9 }
|
||
expect_ascii: "track-9.gpx"
|
||
- input: { name: "Очень-длинное-имя-".ljust(200, "X"), id: 1 }
|
||
expect_max_basename_len: 64
|
||
|
||
- id: TC-UNIT-05
|
||
title: "copyright — OSM ODbL"
|
||
covers:
|
||
ac: [AC-08]
|
||
req: [REQ-F-08]
|
||
given: "track.sources_json = '[\"osm\"]'"
|
||
when: "build_gpx"
|
||
then:
|
||
- "metadata/copyright@author == 'OpenStreetMap contributors'"
|
||
- "metadata/copyright/license == 'https://opendatacommons.org/licenses/odbl/'"
|
||
|
||
- id: TC-UNIT-06
|
||
title: "copyright — Wikiloc"
|
||
covers:
|
||
ac: [AC-09]
|
||
req: [REQ-F-08]
|
||
given: "track.sources_json = '[\"wikiloc\"]'"
|
||
when: "build_gpx"
|
||
then:
|
||
- "metadata/copyright@author == 'Wikiloc contributors'"
|
||
- "metadata/copyright/license содержит wikiloc.com/wikiloc/legalNotice"
|
||
|
||
- id: TC-UNIT-07
|
||
title: "popup HTML — кнопка не рендерится при отсутствии id"
|
||
covers:
|
||
ac: [AC-19]
|
||
req: [REQ-F-13]
|
||
framework_override: jest # JS unit
|
||
given: "_renderTrackPopupHtml({ name: 'X' }) // без id"
|
||
when: "вызывается функция"
|
||
then:
|
||
- "результат НЕ содержит класс 'track-popup-download'"
|
||
- "результат не содержит подстроки '/api/gps-tracks//'"
|
||
|
||
- id: TC-UNIT-08
|
||
title: "XML escaping — name с угловыми скобками и амперсандом"
|
||
covers:
|
||
ac: [AC-20]
|
||
req: [REQ-F-10, NF-05]
|
||
given: "track.name = '\"><script>alert(1)</script>'"
|
||
when: "build_gpx → bytes → fromstring(...)"
|
||
then:
|
||
- "ET.fromstring не выбрасывает исключение"
|
||
- "metadata/name.text == исходная строка"
|
||
- "в bytes нет тега <script (литерально)"
|
||
|
||
- id: TC-UNIT-09
|
||
title: "gpx_builder — copyright опущен если нет известного source"
|
||
covers:
|
||
ac: [AC-08]
|
||
req: [REQ-F-08]
|
||
given: "track.sources_json = '[\"unknown-source\"]'"
|
||
when: "build_gpx"
|
||
then:
|
||
- "metadata/copyright отсутствует"
|
||
|
||
- id: TC-UNIT-10
|
||
title: "popup HTML — кнопка содержит корректный basePath и id"
|
||
covers:
|
||
ac: [AC-01, AC-03]
|
||
req: [REQ-F-12]
|
||
framework_override: jest
|
||
given: "window.location.pathname = '/enduro/', feature.properties = { id: 42, name: 'X' }"
|
||
when: "_renderTrackPopupHtml(props)"
|
||
then:
|
||
- "html содержит href='/enduro/api/gps-tracks/42.gpx'"
|
||
- "html содержит 'Скачать GPX'"
|
||
- "html содержит атрибут download="
|
||
|
||
integration:
|
||
framework: pytest + TestClient
|
||
path: tests/integration/
|
||
|
||
cases:
|
||
|
||
- id: TC-INT-01
|
||
title: "GET .gpx — 200 OK + правильные headers"
|
||
covers:
|
||
ac: [AC-04]
|
||
req: [REQ-F-04]
|
||
setup: "в тестовую sqlite вставлен трек id=1 c name='Test', 5 точек"
|
||
when: "GET /api/gps-tracks/1.gpx"
|
||
then:
|
||
- "status == 200"
|
||
- "header Content-Type == 'application/gpx+xml; charset=utf-8'"
|
||
- "header Content-Disposition содержит 'attachment'"
|
||
- "header Content-Disposition содержит 'filename='"
|
||
- "header Content-Disposition содержит 'filename*=UTF-8'\"'\"''"
|
||
- "header Cache-Control == 'public, max-age=86400'"
|
||
- "header Access-Control-Allow-Origin == '*'"
|
||
- "body начинается с '<?xml'"
|
||
|
||
- id: TC-INT-02
|
||
title: "Content-Disposition — RFC 5987 UTF-8 для кириллицы"
|
||
covers:
|
||
ac: [AC-07]
|
||
req: [REQ-F-05]
|
||
setup: "трек с name='Кольцо вокруг Малинок'"
|
||
when: "GET /api/gps-tracks/{id}.gpx"
|
||
then:
|
||
- "header содержит filename='ascii-fallback'"
|
||
- "header содержит filename*=UTF-8''%D0%9A%D0%BE%D0%BB%D1%8C..."
|
||
|
||
- id: TC-INT-03
|
||
title: "GET .gpx — 404 при NULL geom"
|
||
covers:
|
||
ac: [AC-10]
|
||
req: [REQ-F-03]
|
||
setup: "трек id=2 с geom=NULL"
|
||
when: "GET /api/gps-tracks/2.gpx"
|
||
then:
|
||
- "status == 404"
|
||
- "body == {'detail': 'Track not found'}"
|
||
|
||
- id: TC-INT-04
|
||
title: "GET .gpx — 404 при несуществующем id"
|
||
covers:
|
||
ac: [AC-11]
|
||
req: [REQ-F-03]
|
||
when: "GET /api/gps-tracks/9999999.gpx"
|
||
then:
|
||
- "status == 404"
|
||
|
||
- id: TC-INT-05
|
||
title: "GET .gpx — 422 при не-int id"
|
||
covers:
|
||
ac: [AC-12]
|
||
req: [REQ-F-02]
|
||
when: "GET /api/gps-tracks/abc.gpx"
|
||
then:
|
||
- "status == 422"
|
||
|
||
- id: TC-INT-06
|
||
title: "Регрессия — соседние эндпоинты не сломаны"
|
||
covers:
|
||
ac: [AC-16]
|
||
req: [NF-08]
|
||
when:
|
||
- "GET /api/gps-tracks?bbox=37,55,38,56"
|
||
- "GET /api/gps-tracks/tiles/9/297/154.mvt"
|
||
- "GET /api/gps-tracks/health"
|
||
then:
|
||
- "все три отвечают 200 как до изменений"
|
||
- "формат ответа идентичен (диф = 0 байт vs baseline)"
|
||
|
||
- id: TC-INT-07
|
||
title: "Cache-Control header выставлен"
|
||
covers:
|
||
ac: [AC-18]
|
||
req: [NF-03]
|
||
when: "GET /api/gps-tracks/{id}.gpx → headers"
|
||
then:
|
||
- "Cache-Control == 'public, max-age=86400'"
|
||
|
||
- id: TC-INT-08
|
||
title: "GPX скачанный с сервера проходит XSD-валидацию"
|
||
covers:
|
||
ac: [AC-04]
|
||
req: [REQ-F-06]
|
||
setup: "произвольный трек id=1; локальная копия GPX 1.1 XSD в tests/fixtures/gpx11.xsd"
|
||
when: "GET .gpx → xmlschema.validate(body, xsd)"
|
||
then:
|
||
- "валидация успешна (нет ошибок)"
|
||
|
||
e2e:
|
||
framework: playwright
|
||
path: tests/e2e/
|
||
base_url: https://openclaw.mva154.duckdns.org/enduro/
|
||
|
||
cases:
|
||
|
||
- id: TC-E2E-01
|
||
title: "Download → upload roundtrip"
|
||
covers:
|
||
ac: [AC-13]
|
||
req: [REQ-F-04, REQ-F-09]
|
||
steps:
|
||
- "включить слой 'Публичные треки'"
|
||
- "зум до 14 на координатах с известными треками"
|
||
- "кликнуть на трек"
|
||
- "перехватить URL загрузки из href кнопки 'Скачать GPX'"
|
||
- "скачать файл через page.context().request().get(url)"
|
||
- "пройти через парсер gpx.js (загрузить через input[type=file] '#gpx-file')"
|
||
then:
|
||
- "появляется sheet-gpx с одним треком"
|
||
- "имя трека совпадает с popup.name"
|
||
- "длина совпадает (±1%) с popup.length_km"
|
||
|
||
- id: TC-E2E-02
|
||
title: "Сетевая ошибка → toast"
|
||
covers:
|
||
ac: [AC-15]
|
||
req: [REQ-F-17]
|
||
steps:
|
||
- "route('**/api/gps-tracks/*.gpx', route => route.fulfill({status:500}))"
|
||
- "включить слой, кликнуть на трек"
|
||
- "кликнуть 'Скачать GPX'"
|
||
then:
|
||
- "появляется toast 'Не удалось скачать трек'"
|
||
- "popup НЕ закрылся"
|
||
|
||
perf:
|
||
framework: pytest + httpx + statistics
|
||
path: tests/perf/
|
||
|
||
cases:
|
||
|
||
- id: TC-PERF-01
|
||
title: "GET .gpx — p95 latency"
|
||
covers:
|
||
ac: [AC-17]
|
||
req: [NF-01]
|
||
setup: "трек id=N с 5000 точек"
|
||
when: "100 sequential GET .gpx"
|
||
then:
|
||
- "p95 ≤ 500 ms"
|
||
- "максимум ≤ 1000 ms"
|
||
|
||
# Итоговые счётчики
|
||
totals:
|
||
unit: 10
|
||
integration: 8
|
||
e2e: 2
|
||
perf: 1
|
||
ui_visual: see 04b-ui-test-cases.md
|
||
|
||
# Тестовые фикстуры
|
||
fixtures:
|
||
- path: tests/fixtures/gps_tracks_test.sqlite
|
||
description: "тестовая БД с 5 треками: 1 osm, 1 wikiloc, 1 enduro_russia, 1 без имени, 1 с NULL geom"
|
||
- path: tests/fixtures/gpx11.xsd
|
||
description: "официальная XSD-схема GPX 1.1 (скачана с topografix.com при подготовке тестов)"
|
||
- path: tests/fixtures/perf_track_5000.json
|
||
description: "трек 5000 точек для perf-теста"
|