Newer
Older
gn-synapse-client-py / tests / unit / test_webhook.py
"""verify_webhook: подпись, freshness, парс конверта."""

from __future__ import annotations

import json

from gnexus_synapse import (
    DEFAULT_MAX_SKEW,
    SIGNATURE_HEADER,
    SynapseWebhookError,
    make_signature,
    verify_webhook,
)

SECRET = "s2s-secret-test"
BODY = json.dumps(
    {
        "event_id": "11111111-1111-1111-1111-111111111111",
        "source": "monitoring",
        "subject": "container",
        "action": "down",
        "priority": "critical",
        "payload": {"container": "api"},
    }
).encode()


def test_happy_path_returns_envelope() -> None:
    header = make_signature(BODY, SECRET, timestamp=1_000_000)
    envelope = verify_webhook(
        BODY, {SIGNATURE_HEADER: header}, SECRET, now=1_000_000
    )
    assert envelope["source"] == "monitoring"
    assert envelope["event_id"] == "11111111-1111-1111-1111-111111111111"


def test_header_search_is_case_insensitive() -> None:
    header = make_signature(BODY, SECRET, timestamp=1_000_000)
    envelope = verify_webhook(
        BODY,
        {"X-GNEXUS-SIGNATURE": header, "X-Synapse-Source": "monitoring"},
        SECRET,
        now=1_000_000,
    )
    assert envelope["action"] == "down"


def test_wrong_secret_rejected() -> None:
    header = make_signature(BODY, "other-secret", timestamp=1_000_000)
    try:
        verify_webhook(BODY, {SIGNATURE_HEADER: header}, SECRET, now=1_000_000)
    except SynapseWebhookError as ex:
        assert "не совпала" in ex.detail
    else:
        raise AssertionError("чужой секрет принят")


def test_replay_rejected() -> None:
    header = make_signature(BODY, SECRET, timestamp=1_000_000)
    try:
        verify_webhook(
            BODY, {SIGNATURE_HEADER: header}, SECRET, now=1_000_000 + DEFAULT_MAX_SKEW + 1
        )
    except SynapseWebhookError as ex:
        assert "replay" in ex.detail or "свежее" in ex.detail
    else:
        raise AssertionError("старая доставка принята")


def test_modified_body_rejected() -> None:
    header = make_signature(BODY, SECRET, timestamp=1_000_000)
    try:
        verify_webhook(BODY + b" ", {SIGNATURE_HEADER: header}, SECRET, now=1_000_000)
    except SynapseWebhookError:
        pass
    else:
        raise AssertionError("искажённое тело принято")


def test_missing_and_broken_headers_rejected() -> None:
    for headers in ({}, {SIGNATURE_HEADER: "t=1"}, {SIGNATURE_HEADER: "garbage"},
                    {SIGNATURE_HEADER: f"t={1_000_000},v1="}):
        try:
            verify_webhook(BODY, dict(headers), SECRET, now=1_000_000)
        except SynapseWebhookError:
            pass
        else:
            raise AssertionError(f'заголовок {headers} принят без подписи')


def test_non_json_body_rejected() -> None:
    header = make_signature(b"not json", SECRET, timestamp=1_000_000)
    try:
        verify_webhook(b"not json", {SIGNATURE_HEADER: header}, SECRET, now=1_000_000)
    except SynapseWebhookError:
        pass
    else:
        raise AssertionError("не-JSON тело принято")


def test_list_body_rejected() -> None:
    header = make_signature(b"[1,2]", SECRET, timestamp=1_000_000)
    try:
        verify_webhook(b"[1,2]", {SIGNATURE_HEADER: header}, SECRET, now=1_000_000)
    except SynapseWebhookError:
        pass
    else:
        raise AssertionError("JSON-массив принят как конверт")