"""Tests for scripts/search.py — instance resolution chain. Covers: * ``_load_toml`` — TOML file loading (Python 3.11+ tomllib / 3.8-3.10 tomli) * ``_read_instance_file`` — config file parsing (.toml single/list/table, .txt per-line / comments / comma-separated / blanks, missing file, corrupted toml) * ``resolve_instances`` — priority chain (CLI > env > config file > empty) and config-file auto-discovery (cwd/home, .toml before .txt) Tests are hermetic: cwd and home are redirected to ``tmp_path`` via monkeypatch so the real user environment never interferes. """ import pytest from pathlib import Path from search import _load_toml, _read_instance_file, resolve_instances # ----- _load_toml ----- def test_load_toml_valid_file(tmp_path): f = tmp_path / "test.toml" f.write_text('key = "value"\nnumber = 42\n', encoding="utf-8") data = _load_toml(f) assert data["key"] == "value" assert data["number"] == 42 def test_load_toml_returns_dict(tmp_path): f = tmp_path / "test.toml" f.write_text('instance = "https://x.example.com"\n', encoding="utf-8") data = _load_toml(f) assert isinstance(data, dict) # ----- _read_instance_file: .toml ----- def test_read_toml_single_instance(tmp_path): """Top-level instance = "url" → parse_instances single URL.""" f = tmp_path / "searxng.toml" f.write_text('instance = "https://x.example.com"\n', encoding="utf-8") assert _read_instance_file(f) == ["https://x.example.com"] def test_read_toml_instances_list(tmp_path): """Top-level instances = ["a", "b"] → normalized list.""" f = tmp_path / "searxng.toml" f.write_text('instances = ["a.com", "b.com"]\n', encoding="utf-8") assert _read_instance_file(f) == ["https://a.com", "https://b.com"] def test_read_toml_searxng_table_instance(tmp_path): """Instance under [searxng] table is read correctly.""" f = tmp_path / "searxng.toml" f.write_text('[searxng]\ninstance = "https://t.example.com"\n', encoding="utf-8") assert _read_instance_file(f) == ["https://t.example.com"] def test_read_corrupted_toml_returns_empty(tmp_path): """Corrupted (unparseable) toml → empty list, no exception raised.""" f = tmp_path / "bad.toml" f.write_text("[searxng\ninstance = broken\n", encoding="utf-8") assert _read_instance_file(f) == [] # ----- _read_instance_file: .txt ----- def test_read_txt_one_url_per_line(tmp_path): f = tmp_path / "instances.txt" f.write_text("https://a.com\nhttps://b.com\n", encoding="utf-8") assert _read_instance_file(f) == ["https://a.com", "https://b.com"] def test_read_txt_skips_comments(tmp_path): f = tmp_path / "instances.txt" f.write_text("# comment\nhttps://a.com\n# another\nhttps://b.com\n", encoding="utf-8") assert _read_instance_file(f) == ["https://a.com", "https://b.com"] def test_read_txt_comma_separated(tmp_path): """Comma-separated URLs on one line are split.""" f = tmp_path / "instances.txt" f.write_text("a.com,b.com\n", encoding="utf-8") assert _read_instance_file(f) == ["https://a.com", "https://b.com"] def test_read_txt_skips_blank_lines(tmp_path): f = tmp_path / "instances.txt" f.write_text("https://a.com\n\n\nhttps://b.com\n", encoding="utf-8") assert _read_instance_file(f) == ["https://a.com", "https://b.com"] # ----- _read_instance_file: edge cases ----- def test_read_nonexistent_file_returns_empty(tmp_path): """Missing file → empty list (FileNotFoundError caught internally).""" f = tmp_path / "nonexistent.toml" assert _read_instance_file(f) == [] # ----- resolve_instances: priority chain ----- def test_resolve_cli_arg_takes_priority_over_env(monkeypatch, tmp_path): """CLI arg wins even when SEARXNG_INSTANCE env var is set.""" monkeypatch.setenv("SEARXNG_INSTANCE", "https://env.example.com") assert resolve_instances("https://cli.example.com") == ["https://cli.example.com"] def test_resolve_env_takes_priority_over_config_file(monkeypatch, tmp_path): """Env var wins over a present config file.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) (tmp_path / "searxng.toml").write_text( 'instance = "https://file.example.com"\n', encoding="utf-8") monkeypatch.setenv("SEARXNG_INSTANCE", "https://env.example.com") assert resolve_instances(None) == ["https://env.example.com"] def test_resolve_no_source_returns_empty(monkeypatch, tmp_path): """No CLI, no env, no config file → empty list.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) assert resolve_instances(None) == [] def test_resolve_cli_none_falls_back_to_env(monkeypatch, tmp_path): """cli_arg=None → use SEARXNG_INSTANCE env var.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setenv("SEARXNG_INSTANCE", "https://env.example.com") assert resolve_instances(None) == ["https://env.example.com"] def test_resolve_no_env_falls_back_to_config_file(monkeypatch, tmp_path): """No CLI, no env → fall back to config file.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) (tmp_path / "searxng.toml").write_text( 'instance = "https://file.example.com"\n', encoding="utf-8") assert resolve_instances(None) == ["https://file.example.com"] # ----- resolve_instances: config file discovery ----- def test_resolve_discovers_cwd_searxng_toml(monkeypatch, tmp_path): """./searxng.toml is auto-discovered.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) (tmp_path / "searxng.toml").write_text( 'instance = "https://cwd.example.com"\n', encoding="utf-8") assert resolve_instances(None) == ["https://cwd.example.com"] def test_resolve_discovers_home_searxng_toml(monkeypatch, tmp_path): """~/.config/searxng-cli/searxng.toml is auto-discovered.""" monkeypatch.chdir(tmp_path) # cwd has no config file home = tmp_path / "fake_home" cfg_dir = home / ".config" / "searxng-cli" cfg_dir.mkdir(parents=True) (cfg_dir / "searxng.toml").write_text( 'instance = "https://home.example.com"\n', encoding="utf-8") monkeypatch.setattr(Path, "home", lambda: home) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) assert resolve_instances(None) == ["https://home.example.com"] def test_resolve_discovers_cwd_instances_txt(monkeypatch, tmp_path): """./instances.txt is auto-discovered when no .toml present.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) (tmp_path / "instances.txt").write_text("https://txt.example.com\n", encoding="utf-8") assert resolve_instances(None) == ["https://txt.example.com"] def test_resolve_prefers_toml_over_txt(monkeypatch, tmp_path): """When both ./searxng.toml and ./instances.txt exist, .toml wins.""" monkeypatch.chdir(tmp_path) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.delenv("SEARXNG_INSTANCE", raising=False) (tmp_path / "searxng.toml").write_text( 'instance = "https://toml.example.com"\n', encoding="utf-8") (tmp_path / "instances.txt").write_text("https://txt.example.com\n", encoding="utf-8") result = resolve_instances(None) assert result == ["https://toml.example.com"]