2026-03-21 12:23:05 +02:00
|
|
|
|
"""
|
|
|
|
|
|
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:
|
2026-03-21 12:43:35 +02:00
|
|
|
|
"""_fetchVapidPublicKey в app.js должна обращаться к /api/push/public-key (canonical URL)."""
|
2026-03-21 12:23:05 +02:00
|
|
|
|
content = APP_JS.read_text(encoding="utf-8")
|
2026-03-21 12:43:35 +02:00
|
|
|
|
assert "/api/push/public-key" in content, (
|
|
|
|
|
|
"app.js не содержит URL '/api/push/public-key' — VAPID ключ не читается через API"
|
2026-03-21 12:23:05 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}"
|
|
|
|
|
|
)
|