Rate-limit the optional agent bearer path after user auth resolution so mixed user/agent routes no longer leave an unthrottled PBKDF2 path. Stop logging token prefixes on agent auth failures and require a locally supplied token for backend/.env.test instead of committing one. Update tests and docs to cover agent bearer fallback, configurable webhook signature headers, and the operator-facing security settings added by the hardening work. Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
3.5 KiB
Python
137 lines
3.5 KiB
Python
# ruff: noqa: INP001
|
|
"""Settings validation tests for auth-mode configuration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.core.auth_mode import AuthMode
|
|
from app.core.config import Settings
|
|
|
|
BASE_URL = "http://localhost:8000"
|
|
|
|
|
|
def test_local_mode_requires_non_empty_token() -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.LOCAL,
|
|
local_auth_token="",
|
|
base_url=BASE_URL,
|
|
)
|
|
|
|
|
|
def test_local_mode_requires_minimum_length() -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.LOCAL,
|
|
local_auth_token="x" * 49,
|
|
base_url=BASE_URL,
|
|
)
|
|
|
|
|
|
def test_local_mode_rejects_placeholder_token() -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="LOCAL_AUTH_TOKEN must be at least 50 characters and non-placeholder when AUTH_MODE=local",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.LOCAL,
|
|
local_auth_token="change-me",
|
|
base_url=BASE_URL,
|
|
)
|
|
|
|
|
|
def test_local_mode_accepts_real_token() -> None:
|
|
token = "a" * 50
|
|
settings = Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.LOCAL,
|
|
local_auth_token=token,
|
|
base_url=BASE_URL,
|
|
)
|
|
|
|
assert settings.auth_mode == AuthMode.LOCAL
|
|
assert settings.local_auth_token == token
|
|
|
|
|
|
def test_clerk_mode_requires_secret_key() -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="CLERK_SECRET_KEY must be set and non-empty when AUTH_MODE=clerk",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.CLERK,
|
|
clerk_secret_key="",
|
|
base_url=BASE_URL,
|
|
)
|
|
|
|
|
|
def test_base_url_required() -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="BASE_URL must be set and non-empty",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.CLERK,
|
|
clerk_secret_key="sk_test",
|
|
base_url=" ",
|
|
)
|
|
|
|
|
|
def test_base_url_field_is_required(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("BASE_URL", raising=False)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.CLERK,
|
|
clerk_secret_key="sk_test",
|
|
)
|
|
|
|
text = str(exc_info.value)
|
|
assert "BASE_URL must be set and non-empty" in text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"base_url",
|
|
[
|
|
"localhost:8000",
|
|
"ws://localhost:8000",
|
|
],
|
|
)
|
|
def test_base_url_requires_absolute_http_url(base_url: str) -> None:
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="BASE_URL must be an absolute http\\(s\\) URL",
|
|
):
|
|
Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.CLERK,
|
|
clerk_secret_key="sk_test",
|
|
base_url=base_url,
|
|
)
|
|
|
|
|
|
def test_base_url_is_normalized_without_trailing_slash() -> None:
|
|
token = "a" * 50
|
|
settings = Settings(
|
|
_env_file=None,
|
|
auth_mode=AuthMode.LOCAL,
|
|
local_auth_token=token,
|
|
base_url="http://localhost:8000/ ",
|
|
)
|
|
|
|
assert settings.base_url == BASE_URL
|