A community based topic aggregation platform built on atproto
1"""
2Tests for Configuration Loader.
3
4Tests loading and validating aggregator configuration.
5"""
6import pytest
7import tempfile
8from pathlib import Path
9
10from src.config import ConfigLoader, ConfigError
11from src.models import AggregatorConfig, FeedConfig
12
13
14@pytest.fixture
15def valid_config_yaml():
16 """Valid configuration YAML."""
17 return """
18coves_api_url: "https://api.coves.social"
19
20feeds:
21 - name: "World News"
22 url: "https://news.kagi.com/world.xml"
23 community_handle: "world-news.coves.social"
24 enabled: true
25
26 - name: "Tech News"
27 url: "https://news.kagi.com/tech.xml"
28 community_handle: "tech.coves.social"
29 enabled: true
30
31 - name: "Science News"
32 url: "https://news.kagi.com/science.xml"
33 community_handle: "science.coves.social"
34 enabled: false
35
36log_level: "info"
37"""
38
39
40@pytest.fixture
41def temp_config_file(valid_config_yaml):
42 """Create a temporary config file."""
43 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
44 f.write(valid_config_yaml)
45 temp_path = Path(f.name)
46 yield temp_path
47 # Cleanup
48 if temp_path.exists():
49 temp_path.unlink()
50
51
52class TestConfigLoader:
53 """Test suite for ConfigLoader."""
54
55 def test_load_valid_config(self, temp_config_file):
56 """Test loading valid configuration."""
57 loader = ConfigLoader(temp_config_file)
58 config = loader.load()
59
60 assert isinstance(config, AggregatorConfig)
61 assert config.coves_api_url == "https://api.coves.social"
62 assert config.log_level == "info"
63 assert len(config.feeds) == 3
64
65 def test_parse_feed_configs(self, temp_config_file):
66 """Test parsing feed configurations."""
67 loader = ConfigLoader(temp_config_file)
68 config = loader.load()
69
70 # Check first feed
71 feed1 = config.feeds[0]
72 assert isinstance(feed1, FeedConfig)
73 assert feed1.name == "World News"
74 assert feed1.url == "https://news.kagi.com/world.xml"
75 assert feed1.community_handle == "world-news.coves.social"
76 assert feed1.enabled is True
77
78 # Check disabled feed
79 feed3 = config.feeds[2]
80 assert feed3.name == "Science News"
81 assert feed3.enabled is False
82
83 def test_get_enabled_feeds_only(self, temp_config_file):
84 """Test getting only enabled feeds."""
85 loader = ConfigLoader(temp_config_file)
86 config = loader.load()
87
88 enabled_feeds = [f for f in config.feeds if f.enabled]
89 assert len(enabled_feeds) == 2
90 assert all(f.enabled for f in enabled_feeds)
91
92 def test_missing_config_file_raises_error(self):
93 """Test that missing config file raises error."""
94 with pytest.raises(ConfigError, match="not found"):
95 loader = ConfigLoader(Path("nonexistent.yaml"))
96 loader.load()
97
98 def test_invalid_yaml_raises_error(self):
99 """Test that invalid YAML raises error."""
100 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
101 f.write("invalid: yaml: content: [[[")
102 temp_path = Path(f.name)
103
104 try:
105 with pytest.raises(ConfigError, match="Failed to parse"):
106 loader = ConfigLoader(temp_path)
107 loader.load()
108 finally:
109 temp_path.unlink()
110
111 def test_missing_required_field_raises_error(self):
112 """Test that missing required fields raise error."""
113 invalid_yaml = """
114feeds:
115 - name: "Test"
116 url: "https://test.xml"
117 # Missing community_handle!
118"""
119 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
120 f.write(invalid_yaml)
121 temp_path = Path(f.name)
122
123 try:
124 with pytest.raises(ConfigError, match="Missing required field"):
125 loader = ConfigLoader(temp_path)
126 loader.load()
127 finally:
128 temp_path.unlink()
129
130 def test_missing_coves_api_url_raises_error(self):
131 """Test that missing coves_api_url raises error."""
132 invalid_yaml = """
133feeds:
134 - name: "Test"
135 url: "https://test.xml"
136 community_handle: "test.coves.social"
137"""
138 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
139 f.write(invalid_yaml)
140 temp_path = Path(f.name)
141
142 try:
143 with pytest.raises(ConfigError, match="coves_api_url"):
144 loader = ConfigLoader(temp_path)
145 loader.load()
146 finally:
147 temp_path.unlink()
148
149 def test_default_log_level(self):
150 """Test that log_level defaults to 'info' if not specified."""
151 minimal_yaml = """
152coves_api_url: "https://api.coves.social"
153feeds:
154 - name: "Test"
155 url: "https://test.xml"
156 community_handle: "test.coves.social"
157"""
158 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
159 f.write(minimal_yaml)
160 temp_path = Path(f.name)
161
162 try:
163 loader = ConfigLoader(temp_path)
164 config = loader.load()
165 assert config.log_level == "info"
166 finally:
167 temp_path.unlink()
168
169 def test_default_enabled_true(self):
170 """Test that feed enabled defaults to True if not specified."""
171 yaml_content = """
172coves_api_url: "https://api.coves.social"
173feeds:
174 - name: "Test"
175 url: "https://test.xml"
176 community_handle: "test.coves.social"
177 # No 'enabled' field
178"""
179 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
180 f.write(yaml_content)
181 temp_path = Path(f.name)
182
183 try:
184 loader = ConfigLoader(temp_path)
185 config = loader.load()
186 assert config.feeds[0].enabled is True
187 finally:
188 temp_path.unlink()
189
190 def test_invalid_url_format_raises_error(self):
191 """Test that invalid URLs raise error."""
192 invalid_yaml = """
193coves_api_url: "https://api.coves.social"
194feeds:
195 - name: "Test"
196 url: "not-a-valid-url"
197 community_handle: "test.coves.social"
198"""
199 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
200 f.write(invalid_yaml)
201 temp_path = Path(f.name)
202
203 try:
204 with pytest.raises(ConfigError, match="Invalid URL"):
205 loader = ConfigLoader(temp_path)
206 loader.load()
207 finally:
208 temp_path.unlink()
209
210 def test_empty_feeds_list_raises_error(self):
211 """Test that empty feeds list raises error."""
212 invalid_yaml = """
213coves_api_url: "https://api.coves.social"
214feeds: []
215"""
216 with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.yaml') as f:
217 f.write(invalid_yaml)
218 temp_path = Path(f.name)
219
220 try:
221 with pytest.raises(ConfigError, match="at least one feed"):
222 loader = ConfigLoader(temp_path)
223 loader.load()
224 finally:
225 temp_path.unlink()
226
227 def test_load_from_env_override(self, temp_config_file, monkeypatch):
228 """Test that environment variables can override config values."""
229 # Set environment variable
230 monkeypatch.setenv("COVES_API_URL", "https://test.coves.social")
231
232 loader = ConfigLoader(temp_config_file)
233 config = loader.load()
234
235 # Should use env var instead of config file
236 assert config.coves_api_url == "https://test.coves.social"
237
238 def test_get_feed_by_url(self, temp_config_file):
239 """Test helper to get feed config by URL."""
240 loader = ConfigLoader(temp_config_file)
241 config = loader.load()
242
243 feed = next((f for f in config.feeds if f.url == "https://news.kagi.com/tech.xml"), None)
244 assert feed is not None
245 assert feed.name == "Tech News"
246 assert feed.community_handle == "tech.coves.social"