Skip to content

Commit 30a8cee

Browse files
author
Tom Softreck
committed
update
1 parent 22b383c commit 30a8cee

2 files changed

Lines changed: 292 additions & 0 deletions

File tree

tests/unit/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Unit tests for the DialogChain package."""
2+
3+
# This file makes the tests/unit directory a Python package
4+
# It can be used to share common test fixtures and utilities
5+
6+
# Import common test fixtures for easier access in test modules
7+
from tests.conftest import (
8+
sample_config,
9+
mock_response,
10+
temp_dir,
11+
sample_config_file
12+
)

tests/unit/test_scanner.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
"""Unit tests for the scanner module."""
2+
import asyncio
3+
import os
4+
import tempfile
5+
from pathlib import Path
6+
from unittest.mock import AsyncMock, MagicMock, patch, mock_open
7+
8+
import pytest
9+
import yaml
10+
11+
from dialogchain import scanner, exceptions
12+
13+
14+
class TestConfigScanner:
15+
"""Test the ConfigScanner class."""
16+
17+
@pytest.fixture
18+
def sample_config(self):
19+
"""Return a sample configuration for testing."""
20+
return {
21+
"version": "1.0",
22+
"name": "test_scanner",
23+
"scanners": [
24+
{
25+
"type": "file",
26+
"path": "/path/to/configs",
27+
"pattern": "*.yaml",
28+
"recursive": True
29+
},
30+
{
31+
"type": "http",
32+
"url": "http://example.com/api/configs",
33+
"interval": 300
34+
}
35+
]
36+
}
37+
38+
@pytest.fixture
39+
def mock_file_scanner(self):
40+
"""Create a mock file scanner."""
41+
scanner = MagicMock()
42+
scanner.scan.return_value = ["config1.yaml", "config2.yaml"]
43+
return scanner
44+
45+
@pytest.fixture
46+
def mock_http_scanner(self):
47+
"""Create a mock HTTP scanner."""
48+
scanner = MagicMock()
49+
scanner.scan.return_value = ["http://example.com/config1.yaml"]
50+
return scanner
51+
52+
def test_scanner_initialization(self, sample_config):
53+
"""Test scanner initialization with config."""
54+
with patch('dialogchain.scanner.create_scanner') as mock_create_scanner:
55+
mock_create_scanner.side_effect = ["file_scanner", "http_scanner"]
56+
57+
config_scanner = scanner.ConfigScanner(sample_config)
58+
59+
assert config_scanner.config == sample_config
60+
assert len(config_scanner.scanners) == 2
61+
assert mock_create_scanner.call_count == 2
62+
63+
@pytest.mark.asyncio
64+
async def test_scan(self, sample_config, mock_file_scanner, mock_http_scanner):
65+
"""Test scanning for configuration files."""
66+
with patch('dialogchain.scanner.create_scanner') as mock_create_scanner:
67+
mock_create_scanner.side_effect = [mock_file_scanner, mock_http_scanner]
68+
69+
config_scanner = scanner.ConfigScanner(sample_config)
70+
results = await config_scanner.scan()
71+
72+
assert len(results) == 3 # 2 from file scanner, 1 from http scanner
73+
assert "config1.yaml" in results
74+
assert "config2.yaml" in results
75+
assert "http://example.com/config1.yaml" in results
76+
77+
mock_file_scanner.scan.assert_awaited_once()
78+
mock_http_scanner.scan.assert_awaited_once()
79+
80+
@pytest.mark.asyncio
81+
async def test_scan_with_error(self, sample_config, mock_file_scanner):
82+
"""Test error handling during scanning."""
83+
# Configure one scanner to raise an exception
84+
mock_file_scanner.scan.side_effect = Exception("Scan failed")
85+
86+
with patch('dialogchain.scanner.create_scanner', return_value=mock_file_scanner):
87+
config_scanner = scanner.ConfigScanner({"scanners": [{"type": "file"}]})
88+
89+
with pytest.raises(exceptions.ScannerError) as exc_info:
90+
await config_scanner.scan()
91+
assert "Scan failed" in str(exc_info.value)
92+
93+
94+
class TestFileScanner:
95+
"""Test the FileScanner class."""
96+
97+
@pytest.fixture
98+
def temp_dir(self):
99+
"""Create a temporary directory with test files."""
100+
with tempfile.TemporaryDirectory() as temp_dir:
101+
# Create test files
102+
os.makedirs(os.path.join(temp_dir, "subdir"))
103+
104+
# Create YAML files
105+
with open(os.path.join(temp_dir, "config1.yaml"), "w") as f:
106+
f.write("key1: value1")
107+
108+
with open(os.path.join(temp_dir, "config2.yaml"), "w") as f:
109+
f.write("key2: value2")
110+
111+
# Create a file that doesn't match the pattern
112+
with open(os.path.join(temp_dir, "notes.txt"), "w") as f:
113+
f.write("Some notes")
114+
115+
# Create a file in subdirectory
116+
os.makedirs(os.path.join(temp_dir, "subdir"))
117+
with open(os.path.join(temp_dir, "subdir", "config3.yaml"), "w") as f:
118+
f.write("key3: value3")
119+
120+
yield temp_dir
121+
122+
@pytest.mark.asyncio
123+
async def test_file_scanner_scan(self, temp_dir):
124+
"""Test file scanning with pattern matching."""
125+
config = {
126+
"type": "file",
127+
"path": temp_dir,
128+
"pattern": "*.yaml",
129+
"recursive": False
130+
}
131+
132+
file_scanner = scanner.FileScanner(config)
133+
results = await file_scanner.scan()
134+
135+
# Should find 2 YAML files in the root directory
136+
assert len(results) == 2
137+
assert any("config1.yaml" in str(p) for p in results)
138+
assert any("config2.yaml" in str(p) for p in results)
139+
assert not any("config3.yaml" in str(p) for p in results) # Not in root dir
140+
141+
@pytest.mark.asyncio
142+
async def test_file_scanner_scan_recursive(self, temp_dir):
143+
"""Test recursive file scanning."""
144+
config = {
145+
"type": "file",
146+
"path": temp_dir,
147+
"pattern": "*.yaml",
148+
"recursive": True
149+
}
150+
151+
file_scanner = scanner.FileScanner(config)
152+
results = await file_scanner.scan()
153+
154+
# Should find all 3 YAML files including subdirectories
155+
assert len(results) == 3
156+
assert any("config1.yaml" in str(p) for p in results)
157+
assert any("config2.yaml" in str(p) for p in results)
158+
assert any("config3.yaml" in str(p) for p in results)
159+
160+
@pytest.mark.asyncio
161+
async def test_file_scanner_nonexistent_path(self):
162+
"""Test file scanning with a non-existent path."""
163+
config = {
164+
"type": "file",
165+
"path": "/nonexistent/path",
166+
"pattern": "*.yaml"
167+
}
168+
169+
file_scanner = scanner.FileScanner(config)
170+
171+
with pytest.raises(exceptions.ScannerError) as exc_info:
172+
await file_scanner.scan()
173+
assert "does not exist" in str(exc_info.value)
174+
175+
176+
class TestHttpScanner:
177+
"""Test the HttpScanner class."""
178+
179+
@pytest.fixture
180+
def mock_response(self):
181+
"""Create a mock HTTP response."""
182+
response = MagicMock()
183+
response.status = 200
184+
response.json.return_value = {
185+
"configs": [
186+
{"name": "config1", "url": "http://example.com/config1.yaml"},
187+
{"name": "config2", "url": "http://example.com/config2.yaml"}
188+
]
189+
}
190+
return response
191+
192+
@pytest.fixture
193+
def mock_session(self, mock_response):
194+
"""Create a mock aiohttp client session."""
195+
session = MagicMock()
196+
session.get.return_value.__aenter__.return_value = mock_response
197+
return session
198+
199+
@pytest.mark.asyncio
200+
async def test_http_scanner_scan(self, mock_session):
201+
"""Test HTTP scanning with a mock session."""
202+
config = {
203+
"type": "http",
204+
"url": "http://example.com/api/configs",
205+
"method": "GET",
206+
"headers": {"Authorization": "Bearer token"},
207+
"timeout": 30
208+
}
209+
210+
with patch('aiohttp.ClientSession', return_value=mock_session):
211+
http_scanner = scanner.HttpScanner(config)
212+
results = await http_scanner.scan()
213+
214+
assert len(results) == 2
215+
assert "http://example.com/config1.yaml" in results
216+
assert "http://example.com/config2.yaml" in results
217+
218+
# Verify the request was made correctly
219+
mock_session.get.assert_called_once_with(
220+
"http://example.com/api/configs",
221+
headers={"Authorization": "Bearer token"},
222+
timeout=30
223+
)
224+
225+
@pytest.mark.asyncio
226+
async def test_http_scanner_error_handling(self):
227+
"""Test error handling in HTTP scanner."""
228+
config = {
229+
"type": "http",
230+
"url": "http://example.com/api/configs"
231+
}
232+
233+
# Mock a failed request
234+
async def mock_get(*args, **kwargs):
235+
raise Exception("Connection failed")
236+
237+
mock_session = MagicMock()
238+
mock_session.get.side_effect = mock_get
239+
240+
with patch('aiohttp.ClientSession', return_value=mock_session):
241+
http_scanner = scanner.HttpScanner(config)
242+
243+
with pytest.raises(exceptions.ScannerError) as exc_info:
244+
await http_scanner.scan()
245+
assert "Connection failed" in str(exc_info.value)
246+
247+
248+
class TestScannerFactory:
249+
"""Test the scanner factory functions."""
250+
251+
def test_create_file_scanner(self):
252+
"""Test creating a file scanner."""
253+
config = {
254+
"type": "file",
255+
"path": "/test/path",
256+
"pattern": "*.yaml"
257+
}
258+
scanner_obj = scanner.create_scanner(config)
259+
assert isinstance(scanner_obj, scanner.FileScanner)
260+
assert scanner_obj.path == Path("/test/path")
261+
262+
def test_create_http_scanner(self):
263+
"""Test creating an HTTP scanner."""
264+
config = {
265+
"type": "http",
266+
"url": "http://example.com/api"
267+
}
268+
scanner_obj = scanner.create_scanner(config)
269+
assert isinstance(scanner_obj, scanner.HttpScanner)
270+
assert scanner_obj.url == "http://example.com/api"
271+
272+
def test_create_unknown_scanner(self):
273+
"""Test creating a scanner with an unknown type."""
274+
config = {
275+
"type": "unknown",
276+
"path": "/test"
277+
}
278+
with pytest.raises(ValueError) as exc_info:
279+
scanner.create_scanner(config)
280+
assert "Unknown scanner type" in str(exc_info.value)

0 commit comments

Comments
 (0)