Разработана первая версия приложений
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.cli.seed_settings import load_seed
|
||||
from app.services import REQUIRED_SETTINGS
|
||||
|
||||
|
||||
def test_production_like_seed_contains_all_mandatory_settings() -> None:
|
||||
path = Path(__file__).resolve().parents[3] / "deployment/app-settings.production-like.yaml"
|
||||
rows = load_seed(path)
|
||||
|
||||
assert REQUIRED_SETTINGS <= {row["setting_key"] for row in rows}
|
||||
assert all(row["record_status"] == "A" for row in rows)
|
||||
|
||||
|
||||
def test_seed_rejects_invalid_typed_value(tmp_path: Path) -> None:
|
||||
path = tmp_path / "settings.yaml"
|
||||
path.write_text(
|
||||
"schema_version: 1\nsettings:\n"
|
||||
" bad.integer: {type: integer, value: nope, public: false}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="integer value expected"):
|
||||
load_seed(path)
|
||||
@@ -0,0 +1,80 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from app.auth import canonical_phone
|
||||
from app.integrations import CircuitBreaker, RateLimiter
|
||||
from app.schemas import (
|
||||
FileMessageRequest,
|
||||
MessageRequest,
|
||||
TextMessageRequest,
|
||||
canonical_fingerprint,
|
||||
decode_cursor,
|
||||
encode_cursor,
|
||||
)
|
||||
|
||||
|
||||
def test_phone_claim_priority_and_e164_validation() -> None:
|
||||
claims = {"phone_number": "+74999591007", "preferred_username": "+12025550123"}
|
||||
assert canonical_phone(claims) == "+74999591007"
|
||||
assert canonical_phone({"phone_number": "89999999999"}) is None
|
||||
|
||||
|
||||
def test_message_discriminated_union() -> None:
|
||||
adapter = TypeAdapter(MessageRequest)
|
||||
assert isinstance(
|
||||
adapter.validate_python({"content_kind": "text", "text": "Здравствуйте"}),
|
||||
TextMessageRequest,
|
||||
)
|
||||
assert isinstance(
|
||||
adapter.validate_python(
|
||||
{
|
||||
"content_kind": "file",
|
||||
"attachment_id": str(uuid.uuid4()),
|
||||
"checksum": "sha256:" + "a" * 64,
|
||||
}
|
||||
),
|
||||
FileMessageRequest,
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
adapter.validate_python(
|
||||
{"content_kind": "text", "text": "", "attachment_id": str(uuid.uuid4())}
|
||||
)
|
||||
|
||||
|
||||
def test_fingerprint_is_canonical_and_user_scoped() -> None:
|
||||
user = uuid.uuid4()
|
||||
first = canonical_fingerprint("post", "/dialogs/{id}", {"id": "1"}, {"b": 2, "a": 1}, user)
|
||||
second = canonical_fingerprint("POST", "/dialogs/{id}", {"id": "1"}, {"a": 1, "b": 2}, user)
|
||||
assert first == second
|
||||
assert first != canonical_fingerprint(
|
||||
"POST", "/dialogs/{id}", {"id": "1"}, {"a": 1, "b": 2}, uuid.uuid4()
|
||||
)
|
||||
|
||||
|
||||
def test_cursor_roundtrip_and_tamper_rejection() -> None:
|
||||
secret = b"test-secret" * 4
|
||||
cursor = encode_cursor({"id": str(uuid.uuid4()), "created_at": "2026-01-01"}, secret)
|
||||
assert decode_cursor(cursor, secret)["created_at"] == "2026-01-01"
|
||||
with pytest.raises(ValueError, match="invalid cursor"):
|
||||
decode_cursor(cursor[:-2] + "aa", secret)
|
||||
|
||||
|
||||
def test_rate_limit_keys_do_not_expose_identity() -> None:
|
||||
key = RateLimiter.key("ip", "203.0.113.7", "public", 60)
|
||||
assert "203.0.113.7" not in key
|
||||
assert key.startswith("han:api:rl:ip:")
|
||||
|
||||
|
||||
def test_circuit_breaker_opens_and_half_opens(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
clock = [10.0]
|
||||
monkeypatch.setattr("app.integrations.time.monotonic", lambda: clock[0])
|
||||
breaker = CircuitBreaker(2, 30)
|
||||
breaker.failure()
|
||||
breaker.failure()
|
||||
assert not breaker.allow()
|
||||
clock[0] = 41.0
|
||||
assert breaker.allow()
|
||||
breaker.success()
|
||||
assert breaker.allow()
|
||||
Reference in New Issue
Block a user