Files
lux_fiscal/backend/tests/test_security.py
T
lauadminandClaude Sonnet 5 2664cb8213 Initial commit: backend scaffold, auth, frontend login
- FastAPI + SQLAlchemy async + Alembic + Postgres backend
- Auth: JWT access + rotating refresh tokens, argon2, roles, audit log
- React 19 + Vite frontend: login page, protected route, auth context
- Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:07:15 +03:00

125 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тесты паролей, JWT и шифрования секретов. Без БД и без сети."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
import jwt
import pytest
from app.core import crypto
from app.core.config import settings
from app.core.security import (
ALGORITHM,
TokenError,
create_access_token,
decode_access_token,
generate_refresh_token,
hash_password,
hash_refresh_token,
verify_password,
)
class TestPasswords:
def test_verifies_correct_password(self) -> None:
assert verify_password("correct horse battery", hash_password("correct horse battery"))
def test_rejects_wrong_password(self) -> None:
assert not verify_password("wrong", hash_password("correct horse battery"))
def test_hash_is_salted(self) -> None:
"""Одинаковые пароли обязаны давать разные хеши."""
assert hash_password("same") != hash_password("same")
def test_rejects_garbage_hash_without_raising(self) -> None:
"""Повреждённый хеш в БД не должен ронять вход с 500."""
assert not verify_password("anything", "not-a-valid-argon2-hash")
class TestAccessToken:
def test_roundtrip_carries_identity(self) -> None:
user_id = uuid.uuid4()
payload = decode_access_token(create_access_token(user_id, "cashier"))
assert payload["sub"] == str(user_id)
assert payload["role"] == "cashier"
def test_rejects_expired_token(self) -> None:
expired = jwt.encode(
{
"sub": str(uuid.uuid4()),
"role": "admin",
"type": "access",
"exp": datetime.now(UTC) - timedelta(minutes=1),
},
settings.secret_key,
algorithm=ALGORITHM,
)
with pytest.raises(TokenError):
decode_access_token(expired)
def test_rejects_token_signed_with_other_key(self) -> None:
forged = jwt.encode(
{
"sub": str(uuid.uuid4()),
"role": "admin",
"type": "access",
"exp": datetime.now(UTC) + timedelta(hours=1),
},
"attacker-key",
algorithm=ALGORITHM,
)
with pytest.raises(TokenError):
decode_access_token(forged)
def test_refresh_token_is_not_accepted_as_access(self) -> None:
"""Ключевая проверка: подмена типа токена не должна давать доступ."""
refresh_shaped = jwt.encode(
{
"sub": str(uuid.uuid4()),
"role": "admin",
"type": "refresh",
"exp": datetime.now(UTC) + timedelta(days=7),
},
settings.secret_key,
algorithm=ALGORITHM,
)
with pytest.raises(TokenError, match="access"):
decode_access_token(refresh_shaped)
class TestRefreshToken:
def test_tokens_are_unique(self) -> None:
assert generate_refresh_token()[0] != generate_refresh_token()[0]
def test_hash_matches_raw_value(self) -> None:
raw, stored = generate_refresh_token()
assert hash_refresh_token(raw) == stored
def test_raw_token_is_not_recoverable_from_hash(self) -> None:
raw, stored = generate_refresh_token()
assert raw not in stored
class TestCrypto:
def test_roundtrip(self) -> None:
secret = "license-key-abc-123"
assert crypto.decrypt(crypto.encrypt(secret)) == secret
def test_ciphertext_hides_plaintext(self) -> None:
assert "license-key" not in crypto.encrypt("license-key-abc-123")
def test_tampered_ciphertext_is_rejected(self) -> None:
token = crypto.encrypt("license-key-abc-123")
tampered = token[:-4] + ("AAAA" if not token.endswith("AAAA") else "BBBB")
with pytest.raises(crypto.DecryptionError):
crypto.decrypt(tampered)
@pytest.mark.parametrize(
("value", "expected"),
[("", ""), ("abc", "***"), ("abcd", "****"), ("abcdefgh", "****efgh")],
)
def test_mask(self, value: str, expected: str) -> None:
assert crypto.mask(value) == expected