ForcePilot/backend/test/unit/channels/test_nostr_config_ext.py

301 lines
12 KiB
Python
Raw Normal View History

from __future__ import annotations
import os
from unittest.mock import patch
import pytest
from yuxi.channels.adapters.nostr.config import (
NostrConfig,
NostrConfigError,
GuardPolicyConfig,
RateLimitConfig,
)
class TestNostrConfigValidation:
def test_invalid_dm_policy_raises(self):
with pytest.raises(NostrConfigError, match="dm_policy"):
NostrConfig(dm_policy="invalid_policy")
def test_invalid_streaming_mode_raises(self):
with pytest.raises(NostrConfigError, match="streaming_mode"):
NostrConfig(streaming_mode="realtime")
def test_invalid_markdown_table_mode_raises(self):
with pytest.raises(NostrConfigError, match="markdown_table_mode"):
NostrConfig(markdown_table_mode="invalid")
def test_whitelist_aliased_to_allowlist(self):
cfg = NostrConfig(dm_policy="whitelist")
assert cfg.dm_policy == "allowlist"
def test_reconnect_interval_too_small_raises(self):
with pytest.raises(NostrConfigError, match="reconnect_interval_sec"):
NostrConfig(reconnect_interval_sec=0)
def test_relay_timeout_too_small_raises(self):
with pytest.raises(NostrConfigError, match="relay_timeout_sec"):
NostrConfig(relay_timeout_sec=0)
def test_degraded_threshold_out_of_range_low(self):
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
NostrConfig(relay_degraded_threshold=0)
def test_degraded_threshold_out_of_range_high(self):
with pytest.raises(NostrConfigError, match="relay_degraded_threshold"):
NostrConfig(relay_degraded_threshold=1.5)
def test_degraded_threshold_valid(self):
cfg = NostrConfig(relay_degraded_threshold=0.8)
assert cfg.relay_degraded_threshold == 0.8
def test_relay_url_with_invalid_prefix_raises(self):
with pytest.raises(NostrConfigError, match="ws:// 或 wss://"):
NostrConfig(relays=["http://bad.relay"])
def test_relay_url_ws_rejected_when_require_tls(self):
with pytest.raises(NostrConfigError, match="明文 ws://"):
NostrConfig(relays=["ws://plain.relay"], require_tls=True)
def test_relay_url_ws_allowed_when_require_tls_false(self):
cfg = NostrConfig(relays=["ws://plain.relay"], require_tls=False)
assert "ws://plain.relay" in cfg.relays
def test_all_valid_dm_policies(self):
for policy in NostrConfig.VALID_DM_POLICIES:
if policy == "whitelist":
continue
cfg = NostrConfig(dm_policy=policy)
assert cfg.dm_policy == policy
def test_all_streaming_modes(self):
for mode in NostrConfig.VALID_STREAMING_MODES:
cfg = NostrConfig(streaming_mode=mode)
assert cfg.streaming_mode == mode
class TestNostrConfigFromEnv:
def test_from_env_defaults(self):
with patch.dict(os.environ, {}, clear=True):
cfg = NostrConfig.from_env()
assert cfg.private_key == ""
assert len(cfg.relays) == 4
assert cfg.dm_policy == "pairing"
assert cfg.nip17_enabled is True
def test_from_env_custom_values(self):
env_vars = {
"NOSTR_PRIVATE_KEY": "nsec1testkey123",
"NOSTR_RELAYS": "wss://custom.relay,wss://custom2.relay",
"NOSTR_DM_POLICY": "open",
"NOSTR_STREAMING_MODE": "progress",
"NOSTR_RECONNECT_INTERVAL_SEC": "10",
"NOSTR_RELAY_TIMEOUT_SEC": "60",
"NOSTR_RELAY_DEGRADED_THRESHOLD": "0.7",
"NOSTR_NIP17_ENABLED": "false",
"NOSTR_NIP42_AUTH_ENABLED": "true",
"NOSTR_BACKFILL_WINDOW_SEC": "300",
"NOSTR_ENABLED": "false",
"NOSTR_NAME": "test_bot",
"NOSTR_SEND_CACHE_SIZE": "200",
"NOSTR_MESSAGE_ORDERING": "true",
"NOSTR_MESSAGE_ORDERING_WINDOW_MS": "1000",
"NOSTR_MULTI_ACCOUNT_ENABLED": "true",
"NOSTR_REQUIRE_TLS": "false",
}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.private_key == "nsec1testkey123"
assert len(cfg.relays) == 2
assert cfg.dm_policy == "open"
assert cfg.streaming_mode == "progress"
assert cfg.reconnect_interval_sec == 10
assert cfg.relay_timeout_sec == 60
assert cfg.relay_degraded_threshold == 0.7
assert cfg.nip17_enabled is False
assert cfg.nip42_auth_enabled is True
assert cfg.backfill_window_sec == 300
assert cfg.enabled is False
assert cfg.name == "test_bot"
assert cfg.send_message_cache_size == 200
assert cfg.message_ordering is True
assert cfg.message_ordering_window_ms == 1000
assert cfg.multi_account_enabled is True
assert cfg.require_tls is False
def test_from_env_bool_variants(self):
variants = [
("1", True), ("true", True), ("TRUE", True), ("yes", True), ("on", True),
("0", False), ("false", False), ("no", False), ("off", False),
]
for val, expected in variants:
env_vars = {"NOSTR_NIP17_ENABLED": val}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.nip17_enabled == expected, f"Failed for value {val}"
def test_from_env_bool_missing_defaults_to_false(self):
with patch.dict(os.environ, {}, clear=True):
cfg = NostrConfig.from_env()
assert cfg.nip42_auth_enabled is False
assert cfg.message_ordering is False
def test_from_env_int_invalid_falls_back_to_default(self):
env_vars = {"NOSTR_RECONNECT_INTERVAL_SEC": "not_a_number"}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.reconnect_interval_sec == 5
def test_from_env_float_invalid_falls_back_to_default(self):
env_vars = {"NOSTR_RELAY_DEGRADED_THRESHOLD": "abc"}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.relay_degraded_threshold == 0.5
def test_from_env_list_splits_commas(self):
env_vars = {"NOSTR_ALLOW_FROM": " pub_a , pub_b , pub_c "}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.allow_from == ["pub_a", "pub_b", "pub_c"]
def test_from_env_list_empty_returns_none(self):
with patch.dict(os.environ, {"NOSTR_ALLOW_FROM": ""}, clear=True):
cfg = NostrConfig.from_env()
assert cfg.allow_from == []
def test_from_env_guard_policy(self):
env_vars = {
"NOSTR_GUARD_ALLOWED_KINDS": "1,4,7",
"NOSTR_GUARD_MAX_CIPHERTEXT_BYTES": "30000",
"NOSTR_GUARD_MAX_FUTURE_SKEW_SEC": "60",
"NOSTR_RATE_LIMIT_WINDOW_MS": "5000",
"NOSTR_RATE_LIMIT_MAX_PER_SENDER": "15",
"NOSTR_RATE_LIMIT_MAX_GLOBAL": "300",
}
with patch.dict(os.environ, env_vars, clear=True):
cfg = NostrConfig.from_env()
assert cfg.guard_policy.allowed_kinds == ["1", "4", "7"]
assert cfg.guard_policy.max_ciphertext_bytes == 30000
assert cfg.guard_policy.max_future_skew_sec == 60
assert cfg.guard_policy.rate_limit.window_ms == 5000
assert cfg.guard_policy.rate_limit.max_per_sender_per_window == 15
assert cfg.guard_policy.rate_limit.max_global_per_window == 300
class TestNostrConfigAccountManagement:
def test_get_account_configs_empty_returns_default(self):
cfg = NostrConfig()
result = cfg.get_account_configs({})
assert "default" in result
assert len(result) == 1
def test_get_account_configs_with_accounts(self):
raw = {
"accounts": {
"account1": {"private_key": "nsec1aaa", "enabled": True},
"account2": {"private_key": "nsec1bbb", "enabled": True},
"disabled_account": {"private_key": "nsec1ccc", "enabled": False},
}
}
cfg = NostrConfig()
result = cfg.get_account_configs(raw)
assert "account1" in result
assert "account2" in result
assert "disabled_account" not in result
assert len(result) == 2
def test_get_account_configs_all_disabled_returns_default(self):
raw = {
"accounts": {
"disabled1": {"enabled": False},
"disabled2": {"enabled": False},
}
}
cfg = NostrConfig()
result = cfg.get_account_configs(raw)
assert "default" in result
assert len(result) == 1
def test_list_account_ids_empty(self):
cfg = NostrConfig()
assert cfg.list_account_ids({}) == ["default"]
def test_list_account_ids_with_accounts(self):
raw = {
"accounts": {
"account_a": {"enabled": True},
"account_b": {"enabled": True},
}
}
cfg = NostrConfig()
result = cfg.list_account_ids(raw)
assert result == ["account_a", "account_b"]
def test_list_account_ids_skips_disabled(self):
raw = {
"accounts": {
"enabled_acc": {"enabled": True},
"disabled_acc": {"enabled": False},
}
}
cfg = NostrConfig()
result = cfg.list_account_ids(raw)
assert result == ["enabled_acc"]
def test_resolve_default_account_id(self):
cfg = NostrConfig()
assert cfg.resolve_default_account_id() == "default"
def test_from_dict_with_flat_rate_limit_legacy(self):
cfg = NostrConfig.from_dict({
"accounts": {
"default": {
"guard_policy": {
"rate_limit_window_ms": 8000,
"rate_limit_max_per_sender_per_window": 25,
"rate_limit_max_global_per_window": 500,
}
}
}
})
assert cfg.guard_policy.rate_limit.window_ms == 8000
assert cfg.guard_policy.rate_limit.max_per_sender_per_window == 25
assert cfg.guard_policy.rate_limit.max_global_per_window == 500
def test_guard_policy_config_defaults(self):
gpc = GuardPolicyConfig()
assert gpc.allowed_kinds == [1, 4, 5, 7, 1059]
assert gpc.max_ciphertext_bytes == 50_000
assert gpc.max_plaintext_bytes == 10_000
assert gpc.max_future_skew_sec == 30
def test_rate_limit_config_defaults(self):
rlc = RateLimitConfig()
assert rlc.window_ms == 10_000
assert rlc.max_per_sender_per_window == 20
assert rlc.max_global_per_window == 200
class TestNostrConfigFromDictEdgeCases:
def test_from_dict_with_zero_backfill(self):
cfg = NostrConfig.from_dict({
"accounts": {"default": {"backfill_window_sec": 0}}
})
assert cfg.backfill_window_sec == 0
def test_from_dict_with_allow_from(self):
cfg = NostrConfig.from_dict({
"accounts": {"default": {"allow_from": ["pub_a", "pub_b"]}}
})
assert cfg.allow_from == ["pub_a", "pub_b"]
def test_from_dict_with_markdown_table_convert(self):
cfg = NostrConfig.from_dict({
"accounts": {"default": {"markdown_table_mode": "convert"}}
})
assert cfg.markdown_table_mode == "convert"
def test_from_dict_degraded_threshold_default(self):
cfg = NostrConfig.from_dict({})
assert cfg.relay_degraded_threshold == 0.5