"""Unit tests for the pure helpers behind add-backend / remove-backend / list-backends.""" from __future__ import annotations from pathlib import Path import pytest from pydantic import SecretStr from neuronetz_gateway.cli.backends import ( add_or_replace, parse, redacted_dump, remove, serialize, to_dict, update_env_file, ) from neuronetz_gateway.config import BackendSpec def _spec( name: str, url: str, token: str | None = None, header: str = "Authorization" ) -> BackendSpec: return BackendSpec( name=name, base_url=url, auth_token=SecretStr(token) if token else None, auth_header=header, ) def test_to_dict_omits_default_header_and_scheme() -> None: out = to_dict(_spec("a", "http://x")) assert out == {"name": "a", "base_url": "http://x"} def test_to_dict_includes_token_when_present() -> None: out = to_dict(_spec("a", "http://x", token="s3cret")) assert out["auth_token"] == "s3cret" def test_to_dict_includes_non_default_header() -> None: out = to_dict(_spec("a", "http://x", token="t", header="X-API-Key")) assert out["auth_header"] == "X-API-Key" def test_redacted_dump_hides_token() -> None: out = redacted_dump(_spec("a", "http://x", token="s3cret")) assert out["auth_token"] == "***" # original to_dict still shows it (this fn is the secrets boundary) assert to_dict(_spec("a", "http://x", token="s3cret"))["auth_token"] == "s3cret" def test_serialize_round_trips_via_parse() -> None: backends = [ _spec("embedded", "http://ollama:11434"), _spec("public", "https://ollama.neuronetz.ai", token="tok123"), ] raw = serialize(backends) parsed = parse(raw) assert [b.name for b in parsed] == ["embedded", "public"] assert parsed[0].auth_token is None assert parsed[1].auth_token is not None assert parsed[1].auth_token.get_secret_value() == "tok123" assert parsed[1].base_url == "https://ollama.neuronetz.ai" def test_parse_empty_returns_empty_list() -> None: assert parse("") == [] assert parse(" ") == [] def test_parse_rejects_non_array() -> None: with pytest.raises(ValueError, match="must be a JSON array"): parse('{"name":"a","base_url":"http://x"}') def test_add_appends_when_name_is_new() -> None: backends = [_spec("a", "http://a")] new = _spec("b", "http://b") out = add_or_replace(backends, new) assert [b.name for b in out] == ["a", "b"] def test_add_rejects_duplicate_without_replace() -> None: backends = [_spec("a", "http://a")] new = _spec("a", "http://a-different") with pytest.raises(ValueError, match="already exists"): add_or_replace(backends, new) def test_add_replaces_in_place_when_replace_true() -> None: backends = [_spec("a", "http://old"), _spec("b", "http://b")] new = _spec("a", "http://new", token="t") out = add_or_replace(backends, new, replace=True) # Order preserved — "a" stays in slot 0, "b" stays in slot 1. assert [b.name for b in out] == ["a", "b"] assert out[0].base_url == "http://new" assert out[0].auth_token is not None assert out[0].auth_token.get_secret_value() == "t" def test_remove_drops_named_backend() -> None: backends = [_spec("a", "http://a"), _spec("b", "http://b")] out = remove(backends, "a") assert [b.name for b in out] == ["b"] def test_remove_rejects_unknown_name() -> None: backends = [_spec("a", "http://a")] with pytest.raises(ValueError, match="no backend named 'ghost'"): remove(backends, "ghost") def test_update_env_file_inserts_when_key_absent(tmp_path: Path) -> None: env = tmp_path / ".env" env.write_text("FOO=1\nBAR=hello\n", encoding="utf-8") update_env_file(env, "[{}]") text = env.read_text(encoding="utf-8") assert "FOO=1" in text assert "BAR=hello" in text assert text.rstrip().endswith("OLLAMA_BACKENDS=[{}]") def test_update_env_file_replaces_existing_line(tmp_path: Path) -> None: env = tmp_path / ".env" env.write_text( "FOO=1\nOLLAMA_BACKENDS=old\nBAR=hello\n", encoding="utf-8" ) update_env_file(env, "new") text = env.read_text(encoding="utf-8") # exactly one OLLAMA_BACKENDS line, with the new value matches = [line for line in text.splitlines() if line.startswith("OLLAMA_BACKENDS=")] assert matches == ["OLLAMA_BACKENDS=new"] # untouched lines preserved assert "FOO=1" in text assert "BAR=hello" in text def test_update_env_file_is_idempotent(tmp_path: Path) -> None: env = tmp_path / ".env" env.write_text("KEEP=this\n", encoding="utf-8") update_env_file(env, "v1") update_env_file(env, "v1") # same value twice text = env.read_text(encoding="utf-8") matches = [line for line in text.splitlines() if line.startswith("OLLAMA_BACKENDS=")] assert matches == ["OLLAMA_BACKENDS=v1"] assert "KEEP=this" in text def test_update_env_file_creates_when_missing(tmp_path: Path) -> None: env = tmp_path / "fresh.env" # doesn't exist yet update_env_file(env, "x") assert env.read_text(encoding="utf-8").rstrip() == "OLLAMA_BACKENDS=x"