"""Tests for config module — Config, RunOptions, load_config, ConfigError.""" import json import pytest from pydantic import ValidationError from config import Config, ConfigError, RunOptions, load_config from schemas import MissingFieldPolicy GQL_URL = 'https://test-graphql.example.com/graphql' PROFILE_UUID = 'test-uuid-1234' REQUIRED_CFG = { 'graphql_url': GQL_URL, 'profile_uuid': PROFILE_UUID, } class TestConfig: def test_valid_config(self): cfg = Config(bearer_token='real-token', **REQUIRED_CFG) assert cfg.bearer_token == 'real-token' assert cfg.graphql_url == GQL_URL assert cfg.profile_uuid == PROFILE_UUID def test_missing_bearer_token_raises(self): with pytest.raises(ValidationError): Config(**REQUIRED_CFG) def test_missing_graphql_url_raises(self): with pytest.raises(ValidationError): Config(bearer_token='tok', profile_uuid=PROFILE_UUID) def test_missing_profile_uuid_raises(self): with pytest.raises(ValidationError): Config(bearer_token='tok', graphql_url=GQL_URL) def test_placeholder_bearer_token_raises(self): with pytest.raises(ValidationError, match='placeholder'): Config(bearer_token='YOUR_BEARER_TOKEN_HERE', **REQUIRED_CFG) def test_empty_bearer_token_raises(self): with pytest.raises(ValidationError): Config(bearer_token='', **REQUIRED_CFG) def test_defaults(self): cfg = Config(bearer_token='tok', **REQUIRED_CFG) assert cfg.throttle_capacity is None assert cfg.throttle_rate is None def test_frozen(self): cfg = Config(bearer_token='tok', **REQUIRED_CFG) with pytest.raises(ValidationError): cfg.bearer_token = 'changed' class TestLoadConfig: def test_valid_config(self, tmp_path): cfg_file = tmp_path / 'config.json' cfg_file.write_text(json.dumps({'bearer_token': 'real-token', **REQUIRED_CFG})) cfg = load_config(str(cfg_file)) assert cfg.bearer_token == 'real-token' assert cfg.graphql_url == GQL_URL def test_missing_file_raises_config_error(self): with pytest.raises(ConfigError, match='not found'): load_config('/nonexistent/config.json') def test_invalid_json_raises_config_error(self, tmp_path): cfg_file = tmp_path / 'bad.json' cfg_file.write_text('not json {{{') with pytest.raises(ConfigError, match='Invalid JSON'): load_config(str(cfg_file)) def test_placeholder_token_raises_config_error(self, tmp_path): cfg_file = tmp_path / 'config.json' cfg_file.write_text( json.dumps({'bearer_token': 'YOUR_BEARER_TOKEN_HERE', **REQUIRED_CFG}) ) with pytest.raises(ConfigError, match='Invalid configuration'): load_config(str(cfg_file)) def test_missing_bearer_token_raises_config_error(self, tmp_path): cfg_file = tmp_path / 'config.json' cfg_file.write_text(json.dumps(REQUIRED_CFG)) with pytest.raises(ConfigError, match='Invalid configuration'): load_config(str(cfg_file)) def test_defaults_for_optional_fields(self, tmp_path): cfg_file = tmp_path / 'config.json' cfg_file.write_text(json.dumps({'bearer_token': 'tok', **REQUIRED_CFG})) cfg = load_config(str(cfg_file)) assert cfg.throttle_capacity is None assert cfg.throttle_rate is None def _config(**overrides) -> Config: return Config(**{'bearer_token': 'tok', **REQUIRED_CFG, **overrides}) class TestRunOptionsResolve: def test_defaults_when_nothing_provided(self): opts = RunOptions.resolve(_config()) assert opts.dry_run is False assert opts.limit is None assert opts.policy == MissingFieldPolicy.DEFAULT assert opts.throttle_capacity == 1 assert opts.throttle_rate == 2.0 assert opts.output is None assert opts.resume is None def test_cli_arg_overrides_config(self): cfg = _config(throttle_capacity=5, throttle_rate=10.0) opts = RunOptions.resolve(cfg, throttle_capacity=3, throttle_rate=1.0) assert opts.throttle_capacity == 3 assert opts.throttle_rate == 1.0 def test_config_overrides_default(self): cfg = _config(throttle_capacity=5, throttle_rate=10.0) opts = RunOptions.resolve(cfg) assert opts.throttle_capacity == 5 assert opts.throttle_rate == 10.0 def test_skip_if_missing_true_maps_to_skip_policy(self): opts = RunOptions.resolve(_config(), skip_if_missing=True) assert opts.policy == MissingFieldPolicy.SKIP def test_skip_if_missing_false_maps_to_default_policy(self): opts = RunOptions.resolve(_config(), skip_if_missing=False) assert opts.policy == MissingFieldPolicy.DEFAULT def test_skip_if_missing_from_config(self): cfg = _config(skip_if_missing=True) opts = RunOptions.resolve(cfg) assert opts.policy == MissingFieldPolicy.SKIP def test_cli_skip_if_missing_overrides_config(self): cfg = _config(skip_if_missing=True) opts = RunOptions.resolve(cfg, skip_if_missing=False) assert opts.policy == MissingFieldPolicy.DEFAULT def test_output_and_resume_passed_through(self): opts = RunOptions.resolve(_config(), output='out.csv', resume='prev.csv') assert opts.output == 'out.csv' assert opts.resume == 'prev.csv' def test_zero_capacity_respected(self): """is not None check must not treat 0 as missing.""" cfg = _config(throttle_capacity=5) opts = RunOptions.resolve(cfg, throttle_capacity=1) assert opts.throttle_capacity == 1