kin: BATON-FIX-016 [TECH DEBT] VAPID public key жёстко вшит как пустая строка в <meta>-тег — требует ручного заполнения при деплое

This commit is contained in:
Gros Frumos 2026-03-21 12:23:05 +02:00
parent 5fe9a603f8
commit 8c4c46ee92
3 changed files with 173 additions and 0 deletions

163
tests/test_fix_016.py Normal file
View file

@ -0,0 +1,163 @@
"""
Tests for BATON-FIX-016: VAPID public key убедиться, что ключ не вшит
как пустая строка в frontend-коде и читается через API.
Acceptance criteria:
1. В frontend-коде нет хардкода пустой строки в качестве VAPID key в <meta>-теге.
2. frontend читает ключ через API /api/vapid-public-key (_fetchVapidPublicKey).
3. GET /api/vapid-public-key возвращает HTTP 200.
4. GET /api/vapid-public-key возвращает JSON с полем vapid_public_key.
5. При наличии конфигурации VAPID_PUBLIC_KEY ответ содержит непустое значение.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("BOT_TOKEN", "test-bot-token")
os.environ.setdefault("CHAT_ID", "-1001234567890")
os.environ.setdefault("WEBHOOK_SECRET", "test-webhook-secret")
os.environ.setdefault("WEBHOOK_URL", "https://example.com/api/webhook/telegram")
os.environ.setdefault("FRONTEND_ORIGIN", "http://localhost:3000")
os.environ.setdefault("ADMIN_TOKEN", "test-admin-token")
os.environ.setdefault("ADMIN_CHAT_ID", "5694335584")
import pytest
from tests.conftest import make_app_client
PROJECT_ROOT = Path(__file__).parent.parent
FRONTEND_DIR = PROJECT_ROOT / "frontend"
INDEX_HTML = FRONTEND_DIR / "index.html"
APP_JS = FRONTEND_DIR / "app.js"
_TEST_VAPID_PUBLIC_KEY = "BFakeVapidPublicKeyForTestingPurposesOnlyBase64UrlEncoded"
# ---------------------------------------------------------------------------
# Criterion 1 — AST: no hardcoded empty VAPID key in <meta> tag (index.html)
# ---------------------------------------------------------------------------
def test_index_html_has_no_vapid_meta_tag_with_empty_content() -> None:
"""index.html не должен содержать <meta>-тег с application-server-key и пустым content."""
content = INDEX_HTML.read_text(encoding="utf-8")
match = re.search(
r'<meta[^>]*(?:application-server-key|vapid)[^>]*content\s*=\s*["\']["\']',
content,
re.IGNORECASE,
)
assert match is None, (
f"index.html содержит <meta>-тег с пустым VAPID ключом: {match.group(0)!r}"
)
def test_index_html_has_no_hardcoded_application_server_key_attribute() -> None:
"""index.html не должен содержать атрибут application-server-key вообще."""
content = INDEX_HTML.read_text(encoding="utf-8")
assert "application-server-key" not in content.lower(), (
"index.html содержит атрибут 'application-server-key'"
"VAPID ключ не должен быть вшит в HTML"
)
# ---------------------------------------------------------------------------
# Criterion 2 — AST: frontend reads key through API (app.js)
# ---------------------------------------------------------------------------
def test_app_js_contains_fetch_vapid_public_key_function() -> None:
"""app.js должен содержать функцию _fetchVapidPublicKey."""
content = APP_JS.read_text(encoding="utf-8")
assert "_fetchVapidPublicKey" in content, (
"app.js не содержит функцию _fetchVapidPublicKey — "
"чтение VAPID ключа через API не реализовано"
)
def test_app_js_fetch_vapid_calls_api_endpoint() -> None:
"""_fetchVapidPublicKey в app.js должна обращаться к /api/vapid-public-key."""
content = APP_JS.read_text(encoding="utf-8")
assert "/api/vapid-public-key" in content, (
"app.js не содержит URL '/api/vapid-public-key' — VAPID ключ не читается через API"
)
def test_app_js_init_push_subscription_has_null_guard() -> None:
"""_initPushSubscription в app.js должна содержать guard против null ключа."""
content = APP_JS.read_text(encoding="utf-8")
assert re.search(r"if\s*\(\s*!\s*vapidPublicKey\s*\)", content), (
"app.js: _initPushSubscription не содержит guard 'if (!vapidPublicKey)'"
"подписка может быть создана без ключа"
)
def test_app_js_init_chains_fetch_vapid_then_init_subscription() -> None:
"""_init() в app.js должна вызывать _fetchVapidPublicKey().then(_initPushSubscription)."""
content = APP_JS.read_text(encoding="utf-8")
assert re.search(
r"_fetchVapidPublicKey\(\)\s*\.\s*then\s*\(\s*_initPushSubscription\s*\)",
content,
), (
"app.js: _init() не содержит цепочку _fetchVapidPublicKey().then(_initPushSubscription)"
)
def test_app_js_has_no_empty_string_hardcoded_as_application_server_key() -> None:
"""app.js не должен содержать хардкода пустой строки для applicationServerKey."""
content = APP_JS.read_text(encoding="utf-8")
match = re.search(r"applicationServerKey\s*[=:]\s*[\"']{2}", content)
assert match is None, (
f"app.js содержит хардкод пустой строки для applicationServerKey: {match.group(0)!r}"
)
# ---------------------------------------------------------------------------
# Criterion 3 — HTTP: GET /api/vapid-public-key returns 200
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_vapid_public_key_endpoint_returns_200() -> None:
"""GET /api/vapid-public-key должен вернуть HTTP 200."""
async with make_app_client() as client:
response = await client.get("/api/vapid-public-key")
assert response.status_code == 200, (
f"GET /api/vapid-public-key вернул {response.status_code}, ожидался 200"
)
# ---------------------------------------------------------------------------
# Criterion 4 — HTTP: response JSON contains vapid_public_key field
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_vapid_public_key_endpoint_returns_json_with_field() -> None:
"""GET /api/vapid-public-key должен вернуть JSON с полем vapid_public_key."""
async with make_app_client() as client:
response = await client.get("/api/vapid-public-key")
data = response.json()
assert "vapid_public_key" in data, (
f"Ответ /api/vapid-public-key не содержит поле 'vapid_public_key': {data!r}"
)
# ---------------------------------------------------------------------------
# Criterion 5 — HTTP: non-empty vapid_public_key when env var is configured
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_vapid_public_key_endpoint_returns_configured_value() -> None:
"""GET /api/vapid-public-key возвращает непустой ключ, когда VAPID_PUBLIC_KEY задан."""
with patch("backend.config.VAPID_PUBLIC_KEY", _TEST_VAPID_PUBLIC_KEY):
async with make_app_client() as client:
response = await client.get("/api/vapid-public-key")
data = response.json()
assert data.get("vapid_public_key") == _TEST_VAPID_PUBLIC_KEY, (
f"vapid_public_key должен быть '{_TEST_VAPID_PUBLIC_KEY}', "
f"получили: {data.get('vapid_public_key')!r}"
)