155 lines
6.4 KiB
Python
155 lines
6.4 KiB
Python
|
|
"""Regression tests for KIN-FIX-024 — dist из worktree вручную скопирован в основной проект.
|
|||
|
|
|
|||
|
|
Acceptance criteria:
|
|||
|
|
- после `git pull` + `npm run build` в main — деплой работает без ручного копирования dist/
|
|||
|
|
|
|||
|
|
Проверяем:
|
|||
|
|
(1) DIST path в api.py указывает на web/frontend/dist (относительно api.py)
|
|||
|
|
(2) В проекте нет dist/ в корне (stray artifact)
|
|||
|
|
(3) В web/ нет dist/ вне web/frontend/dist/ (stray artifact)
|
|||
|
|
(4) tsconfig.app.json исключает *.test.ts из сборки
|
|||
|
|
(5) tsconfig.app.json исключает *.spec.ts из сборки
|
|||
|
|
(6) tsconfig.app.json исключает __tests__/ директории
|
|||
|
|
(7) GET / → возвращает index.html из dist/ (SPA root)
|
|||
|
|
(8) GET /some/nested/route → SPA fallback на index.html
|
|||
|
|
(9) GET /favicon.ico → конкретный файл из dist/ (не index.html)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import pytest
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|||
|
|
WEB_API_FILE = PROJECT_ROOT / "web" / "api.py"
|
|||
|
|
TSCONFIG_APP = PROJECT_ROOT / "web" / "frontend" / "tsconfig.app.json"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# Fixtures
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def spa_client(tmp_path):
|
|||
|
|
"""TestClient с изолированной БД и фейковым dist/ для тестов SPA endpoint."""
|
|||
|
|
import web.api as api_module
|
|||
|
|
|
|||
|
|
db_path = tmp_path / "test.db"
|
|||
|
|
api_module.DB_PATH = db_path
|
|||
|
|
|
|||
|
|
# Создаём фейковый dist/
|
|||
|
|
dist_dir = tmp_path / "dist"
|
|||
|
|
dist_dir.mkdir()
|
|||
|
|
(dist_dir / "index.html").write_text("<html><body>SPA</body></html>")
|
|||
|
|
(dist_dir / "favicon.ico").write_bytes(b"ICON_DATA")
|
|||
|
|
|
|||
|
|
original_dist = api_module.DIST
|
|||
|
|
api_module.DIST = dist_dir
|
|||
|
|
|
|||
|
|
from web.api import app
|
|||
|
|
from fastapi.testclient import TestClient
|
|||
|
|
client = TestClient(app)
|
|||
|
|
yield client
|
|||
|
|
|
|||
|
|
api_module.DIST = original_dist
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# (1–3) Проверка расположения dist/ в проекте
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def test_dist_path_points_to_web_frontend_dist():
|
|||
|
|
"""api.py DIST должен резолвиться в web/frontend/dist относительно api.py."""
|
|||
|
|
import web.api as api_module
|
|||
|
|
expected = (WEB_API_FILE.parent / "frontend" / "dist").resolve()
|
|||
|
|
actual = api_module.DIST.resolve()
|
|||
|
|
assert actual == expected, (
|
|||
|
|
f"DIST указывает на {actual}, ожидалось {expected}. "
|
|||
|
|
"Возможна ошибка пути — деплой без ручного копирования не будет работать."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_no_stray_dist_in_project_root():
|
|||
|
|
"""dist/ не должен существовать в корне проекта (признак ручного копирования)."""
|
|||
|
|
root_dist = PROJECT_ROOT / "dist"
|
|||
|
|
assert not root_dist.exists(), (
|
|||
|
|
f"Обнаружен stray dist/ в корне проекта: {root_dist}. "
|
|||
|
|
"Это признак ручного копирования из worktree."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_no_stray_dist_in_web_root():
|
|||
|
|
"""dist/ не должен существовать в web/ (только в web/frontend/dist/)."""
|
|||
|
|
web_dist = PROJECT_ROOT / "web" / "dist"
|
|||
|
|
assert not web_dist.exists(), (
|
|||
|
|
f"Обнаружен stray dist/ в web/: {web_dist}. "
|
|||
|
|
"Правильное место: web/frontend/dist/"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# (4–6) tsconfig.app.json — exclude для тест-файлов
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def _load_tsconfig_exclude() -> list:
|
|||
|
|
"""Парсим tsconfig.app.json как JSONC (может содержать /* */ и // комментарии)."""
|
|||
|
|
content = TSCONFIG_APP.read_text()
|
|||
|
|
# Стрипаем блочные комментарии /* ... */
|
|||
|
|
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
|||
|
|
# Стрипаем строчные комментарии // ...
|
|||
|
|
content = re.sub(r"//[^\n]*", "", content)
|
|||
|
|
data = json.loads(content)
|
|||
|
|
return data.get("exclude", [])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_tsconfig_app_excludes_test_ts():
|
|||
|
|
"""tsconfig.app.json должен исключать src/**/*.test.ts из сборки."""
|
|||
|
|
exclude = _load_tsconfig_exclude()
|
|||
|
|
assert any("test.ts" in e for e in exclude), (
|
|||
|
|
f"tsconfig.app.json не исключает *.test.ts — тест-файлы попадут в сборку. "
|
|||
|
|
f"exclude={exclude}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_tsconfig_app_excludes_spec_ts():
|
|||
|
|
"""tsconfig.app.json должен исключать src/**/*.spec.ts из сборки."""
|
|||
|
|
exclude = _load_tsconfig_exclude()
|
|||
|
|
assert any("spec.ts" in e for e in exclude), (
|
|||
|
|
f"tsconfig.app.json не исключает *.spec.ts — тест-файлы попадут в сборку. "
|
|||
|
|
f"exclude={exclude}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_tsconfig_app_excludes_tests_directory():
|
|||
|
|
"""tsconfig.app.json должен исключать src/**/__tests__/ из сборки."""
|
|||
|
|
exclude = _load_tsconfig_exclude()
|
|||
|
|
assert any("__tests__" in e for e in exclude), (
|
|||
|
|
f"tsconfig.app.json не исключает __tests__/ директории. "
|
|||
|
|
f"exclude={exclude}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# (7–9) SPA endpoint — serve_spa обслуживает из корректного dist/
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def test_serve_spa_root_returns_index_html(spa_client):
|
|||
|
|
"""GET / → index.html из dist/ (SPA entry point)."""
|
|||
|
|
r = spa_client.get("/")
|
|||
|
|
assert r.status_code == 200
|
|||
|
|
assert "SPA" in r.text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_serve_spa_unknown_route_returns_index_html(spa_client):
|
|||
|
|
"""GET /projects/kin/tasks → SPA fallback (index.html), не 404."""
|
|||
|
|
r = spa_client.get("/projects/kin/tasks")
|
|||
|
|
assert r.status_code == 200
|
|||
|
|
assert "SPA" in r.text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_serve_spa_existing_file_returned_directly(spa_client):
|
|||
|
|
"""GET /favicon.ico → конкретный файл из dist/, а не index.html."""
|
|||
|
|
r = spa_client.get("/favicon.ico")
|
|||
|
|
assert r.status_code == 200
|
|||
|
|
assert r.content == b"ICON_DATA"
|