"""Tests for HTTPEndpointStore — CRUD + auth_token encryption + header policy.""" import pytest from ledgrab.storage.database import Database from ledgrab.storage.http_endpoint_store import HTTPEndpointStore from ledgrab.utils import secret_box @pytest.fixture def http_store(tmp_path): db = Database(tmp_path / "test.db") store = HTTPEndpointStore(db) yield store db.close() class TestCRUD: def test_create_basic(self, http_store): e = http_store.create_endpoint( name="Plex", url="https://plex.example.com/status/sessions", ) assert e.id.startswith("htep_") assert e.url == "https://plex.example.com/status/sessions" assert e.method == "GET" assert e.timeout_s == 10.0 assert e.headers == {} assert e.auth_token == "" def test_create_with_auth_and_headers(self, http_store): e = http_store.create_endpoint( name="Plex", url="https://plex.example.com/status/sessions", auth_token="my-secret-token", headers={"Accept": "application/json"}, timeout_s=5.0, ) assert e.auth_token == "my-secret-token" assert e.headers == {"Accept": "application/json"} assert e.timeout_s == 5.0 def test_create_requires_url(self, http_store): with pytest.raises(ValueError, match="url is required"): http_store.create_endpoint(name="x", url="") def test_create_rejects_unsupported_method(self, http_store): with pytest.raises(ValueError, match="Unsupported method"): http_store.create_endpoint(name="x", url="https://example.com", method="POST") def test_create_rejects_zero_timeout(self, http_store): with pytest.raises(ValueError, match="timeout_s"): http_store.create_endpoint(name="x", url="https://example.com", timeout_s=0) def test_unique_name(self, http_store): http_store.create_endpoint(name="dup", url="https://a") with pytest.raises(ValueError, match="already exists"): http_store.create_endpoint(name="dup", url="https://b") def test_update(self, http_store): e = http_store.create_endpoint(name="Plex", url="https://a") updated = http_store.update_endpoint( e.id, url="https://b", auth_token="new-token", ) assert updated.url == "https://b" assert updated.auth_token == "new-token" assert updated.created_at == e.created_at assert updated.updated_at > e.updated_at def test_update_with_empty_token_clears(self, tmp_path): """Sending ``auth_token=""`` MUST clear the stored token (the endpoint route distinguishes None=keep from ""=clear; the store layer is the source of truth for the on-disk state).""" db = Database(tmp_path / "test.db") store = HTTPEndpointStore(db) e = store.create_endpoint(name="x", url="https://a", auth_token="initial-secret") assert e.auth_token == "initial-secret" cleared = store.update_endpoint(e.id, auth_token="") assert cleared.auth_token == "" # Re-open the DB to confirm the change is persisted, not just in # the in-memory cache. db.close() db2 = Database(tmp_path / "test.db") store2 = HTTPEndpointStore(db2) reloaded = store2.get_endpoint(e.id) assert reloaded.auth_token == "" db2.close() def test_delete(self, http_store): e = http_store.create_endpoint(name="x", url="https://a") http_store.delete_endpoint(e.id) from ledgrab.storage.base_store import EntityNotFoundError with pytest.raises(EntityNotFoundError): http_store.get_endpoint(e.id) class TestTokenEncryption: """auth_token must be encrypted at rest, decrypted on load.""" def test_token_encrypted_at_rest(self, tmp_path): db = Database(tmp_path / "test.db") store = HTTPEndpointStore(db) store.create_endpoint(name="x", url="https://a", auth_token="plaintext-secret") rows = db.load_all("http_endpoints") assert len(rows) == 1 raw = rows[0]["auth_token"] assert raw != "plaintext-secret" assert secret_box.is_encrypted(raw) db.close() def test_token_decrypted_on_load(self, tmp_path): db_path = tmp_path / "test.db" db = Database(db_path) store = HTTPEndpointStore(db) eid = store.create_endpoint(name="x", url="https://a", auth_token="round-trip").id db.close() db2 = Database(db_path) store2 = HTTPEndpointStore(db2) loaded = store2.get_endpoint(eid) assert loaded.auth_token == "round-trip" db2.close() class TestHeadersAndAuth: def test_build_request_headers_with_token(self, http_store): e = http_store.create_endpoint( name="x", url="https://a", auth_token="abc", headers={"Accept": "application/json"}, ) h = e.build_request_headers() assert h["Authorization"] == "Bearer abc" assert h["Accept"] == "application/json" def test_custom_authorization_header_wins(self, http_store): e = http_store.create_endpoint( name="x", url="https://a", auth_token="bearer-token", headers={"Authorization": "Basic dXNlcjpwYXNz"}, ) h = e.build_request_headers() assert h["Authorization"] == "Basic dXNlcjpwYXNz" def test_case_insensitive_authorization_check(self, http_store): """Lowercase 'authorization' supplied by the user must win too.""" e = http_store.create_endpoint( name="x", url="https://a", auth_token="should-not-be-sent", headers={"authorization": "Basic xyz"}, ) h = e.build_request_headers() # No second Authorization key — user's lowercase variant is preserved as-is assert h.get("authorization") == "Basic xyz" assert "Authorization" not in h def test_no_auth_no_header(self, http_store): e = http_store.create_endpoint(name="x", url="https://a") assert "Authorization" not in e.build_request_headers()