fix: resolve comprehensive review findings (security, concurrency, perf, Android, UI)
Multi-dimension review of v0.8.2. Excludes the deliberately deferred default_config.yaml weak-default-key item (C1). Backend: - calibration: create_default_calibration no longer exceeds led_count for small odd counts (bounded trim + regression test) - game-integration: generic webhook now requires auth_token; constant-time compare_digest in all adapters; per-IP failed-auth rate limit on the ingest route; auth_token encrypted at rest via secret_box (migration-safe) - playlist engine: serialize _state/_task under the lifecycle lock to close a delete-mid-play race (+ concurrency tests) - main: stop the calibration session on shutdown (restore prior target) - home_assistant: validate HA host via the LAN classifier on create/update - perf: drop slow preview-WS clients instead of blocking the send loop; cache composite full-strip resize linspaces; effect_stream lava reuses scratch Frontend: - setup/auto-calibration wizard: guard _state after awaits (cancel-safe), await session teardown before output start, busy-gate skip-calibration, manual display input keeps focus, move focus on step change - calibration: destroy EntitySelect on modal close - color-strips test: dirty-flag-gated render + cached ctx/ImageData - a11y/TV: focus-visible for new wizard/auto-cal/corner controls, aria-labels on the spatial corner/edge picker; theme-aware syntax tokens; dead/undefined CSS tokens removed; .modal-error styled; i18n titles (en/ru/zh) Android: - ApiKeyManager: EncryptedSharedPreferences with verified, data-safe legacy migration that never rotates an existing key - CaptureService: validate MediaProjection token before promoting; satisfy the startForeground 5s contract on the bail path - NotificationListener: connection-scoped executor with lazy fallback - BLE: request BLUETOOTH_SCAN/CONNECT at runtime + guard handler-thread SecurityExceptions - Root: cancellation-aware su grant probe Adds 14 tests. Gate: ruff + tsc 5.9.3 + esbuild + pytest (2185 passed) + compileDebugKotlin all green.
This commit is contained in:
@@ -5,6 +5,7 @@ import pytest
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.game_integration import EventMapping, GameIntegrationConfig
|
||||
from ledgrab.storage.game_integration_store import GameIntegrationStore
|
||||
from ledgrab.utils import secret_box
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -272,3 +273,83 @@ class TestGetReferences:
|
||||
config = store.create_integration(name="Test", adapter_type="webhook")
|
||||
refs = store.get_references(config.id)
|
||||
assert refs == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Secret encryption (adapter_config.auth_token) tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAdapterConfigSecretEncryption:
|
||||
def test_auth_token_round_trips(self):
|
||||
config = GameIntegrationConfig.create_from_kwargs(
|
||||
name="Webhook",
|
||||
adapter_type="generic_webhook",
|
||||
adapter_config={"auth_token": "super-secret", "auth_header": "X-Token"},
|
||||
)
|
||||
restored = GameIntegrationConfig.from_dict(config.to_dict())
|
||||
# Decrypted at runtime — plaintext is recovered.
|
||||
assert restored.adapter_config["auth_token"] == "super-secret"
|
||||
# Non-secret fields are untouched.
|
||||
assert restored.adapter_config["auth_header"] == "X-Token"
|
||||
|
||||
def test_stored_token_is_not_plaintext(self):
|
||||
config = GameIntegrationConfig.create_from_kwargs(
|
||||
name="Webhook",
|
||||
adapter_type="generic_webhook",
|
||||
adapter_config={"auth_token": "plaintext-secret"},
|
||||
)
|
||||
stored = config.to_dict()
|
||||
raw_token = stored["adapter_config"]["auth_token"]
|
||||
assert raw_token != "plaintext-secret"
|
||||
assert secret_box.is_encrypted(raw_token)
|
||||
|
||||
def test_legacy_plaintext_row_still_decrypts(self):
|
||||
# Simulate a row written before encryption was added: auth_token is
|
||||
# bare plaintext (not an ENC: envelope). It must still load.
|
||||
legacy = {
|
||||
"id": "gi_legacy01",
|
||||
"name": "Legacy Webhook",
|
||||
"adapter_type": "generic_webhook",
|
||||
"enabled": True,
|
||||
"adapter_config": {"auth_token": "legacy-plaintext"},
|
||||
"event_mappings": [],
|
||||
"created_at": "2024-01-01T00:00:00+00:00",
|
||||
"updated_at": "2024-01-01T00:00:00+00:00",
|
||||
}
|
||||
restored = GameIntegrationConfig.from_dict(legacy)
|
||||
assert restored.adapter_config["auth_token"] == "legacy-plaintext"
|
||||
|
||||
def test_no_auth_token_key_is_safe(self):
|
||||
config = GameIntegrationConfig.create_from_kwargs(
|
||||
name="NoSecret",
|
||||
adapter_type="cs2",
|
||||
adapter_config={"max_gold": 99999},
|
||||
)
|
||||
restored = GameIntegrationConfig.from_dict(config.to_dict())
|
||||
assert restored.adapter_config == {"max_gold": 99999}
|
||||
|
||||
def test_db_stores_token_encrypted(self, tmp_db):
|
||||
store = GameIntegrationStore(tmp_db)
|
||||
store.create_integration(
|
||||
name="Webhook",
|
||||
adapter_type="generic_webhook",
|
||||
adapter_config={"auth_token": "db-secret"},
|
||||
)
|
||||
rows = tmp_db.load_all("game_integrations")
|
||||
assert len(rows) == 1
|
||||
raw_token = rows[0]["adapter_config"]["auth_token"]
|
||||
assert raw_token != "db-secret"
|
||||
assert secret_box.is_encrypted(raw_token)
|
||||
|
||||
def test_db_round_trip_after_reload(self, tmp_db):
|
||||
store1 = GameIntegrationStore(tmp_db)
|
||||
created = store1.create_integration(
|
||||
name="Webhook",
|
||||
adapter_type="generic_webhook",
|
||||
adapter_config={"auth_token": "reload-secret"},
|
||||
)
|
||||
# New store instance reads from disk and must decrypt.
|
||||
store2 = GameIntegrationStore(tmp_db)
|
||||
fetched = store2.get_integration(created.id)
|
||||
assert fetched.adapter_config["auth_token"] == "reload-secret"
|
||||
|
||||
Reference in New Issue
Block a user