Skip to content

Commit 5a222ec

Browse files
committed
test: add comprehensive test suite for all dns_utils modules
Made-with: Cursor
1 parent c041fa0 commit 5a222ec

20 files changed

Lines changed: 6316 additions & 0 deletions

.coveragerc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[run]
2+
source = dns_utils
3+
omit =
4+
build_setup.py
5+
tests/*
6+
branch = true
7+
8+
[report]
9+
fail_under = 90
10+
show_missing = true
11+
exclude_lines =
12+
pragma: no cover
13+
def __repr__
14+
raise NotImplementedError
15+
if __name__ == .__main__.:
16+
pass$

.github/workflows/test.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
test:
11+
name: Test Python ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
cache: "pip"
26+
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
pip install -r requirements-dev.txt
31+
32+
- name: Run tests with coverage
33+
run: |
34+
python -m pytest tests/ \
35+
--cov=dns_utils \
36+
--cov-report=term-missing \
37+
--cov-report=xml \
38+
--cov-fail-under=90 \
39+
-v
40+
41+
- name: Upload coverage report
42+
uses: actions/upload-artifact@v4
43+
if: always()
44+
with:
45+
name: coverage-${{ matrix.python-version }}
46+
path: coverage.xml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ logs/
2525
*.tmp
2626
*.exe
2727
build/
28+
.hypothesis/
29+
.coverage

pytest.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[pytest]
2+
testpaths = tests
3+
asyncio_mode = auto
4+
timeout = 30
5+
addopts = -v

requirements-dev.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-r requirements.txt
2+
3+
pytest
4+
pytest-asyncio
5+
pytest-timeout
6+
pytest-xdist
7+
pytest-mock
8+
pytest-cov
9+
hypothesis
10+
black
11+
isort
12+
mypy
13+
pylint
14+
autopep8

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""Shared test fixtures for MasterDnsVPN test suite."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from typing import Any
7+
from unittest.mock import AsyncMock, MagicMock
8+
9+
import pytest
10+
11+
from dns_utils.DnsPacketParser import DnsPacketParser
12+
13+
14+
# ---------------------------------------------------------------------------
15+
# Logger fixtures
16+
# ---------------------------------------------------------------------------
17+
18+
19+
class MockLogger:
20+
"""Simple logger that records calls for assertion."""
21+
22+
def __init__(self) -> None:
23+
self.debug_calls: list[str] = []
24+
self.info_calls: list[str] = []
25+
self.warning_calls: list[str] = []
26+
self.error_calls: list[str] = []
27+
28+
def debug(self, msg: Any, *args: Any, **kwargs: Any) -> None:
29+
self.debug_calls.append(str(msg))
30+
31+
def info(self, msg: Any, *args: Any, **kwargs: Any) -> None:
32+
self.info_calls.append(str(msg))
33+
34+
def warning(self, msg: Any, *args: Any, **kwargs: Any) -> None:
35+
self.warning_calls.append(str(msg))
36+
37+
def error(self, msg: Any, *args: Any, **kwargs: Any) -> None:
38+
self.error_calls.append(str(msg))
39+
40+
def opt(self, **kwargs: Any) -> "MockLogger":
41+
return self
42+
43+
44+
@pytest.fixture
45+
def mock_logger() -> MockLogger:
46+
return MockLogger()
47+
48+
49+
# ---------------------------------------------------------------------------
50+
# DnsPacketParser fixtures
51+
# ---------------------------------------------------------------------------
52+
53+
54+
@pytest.fixture
55+
def parser_no_crypto(mock_logger: MockLogger) -> DnsPacketParser:
56+
"""DnsPacketParser with encryption disabled (method 0)."""
57+
return DnsPacketParser(
58+
logger=mock_logger,
59+
encryption_key="testkey",
60+
encryption_method=0,
61+
)
62+
63+
64+
@pytest.fixture
65+
def parser_xor(mock_logger: MockLogger) -> DnsPacketParser:
66+
"""DnsPacketParser with XOR encryption (method 1)."""
67+
return DnsPacketParser(
68+
logger=mock_logger,
69+
encryption_key="testkey",
70+
encryption_method=1,
71+
)
72+
73+
74+
@pytest.fixture
75+
def parser_chacha20(mock_logger: MockLogger) -> DnsPacketParser:
76+
"""DnsPacketParser with ChaCha20 encryption (method 2)."""
77+
return DnsPacketParser(
78+
logger=mock_logger,
79+
encryption_key="testkey1234567890",
80+
encryption_method=2,
81+
)
82+
83+
84+
@pytest.fixture
85+
def parser_aes128(mock_logger: MockLogger) -> DnsPacketParser:
86+
"""DnsPacketParser with AES-128-GCM (method 3)."""
87+
return DnsPacketParser(
88+
logger=mock_logger,
89+
encryption_key="testkey1234567890",
90+
encryption_method=3,
91+
)
92+
93+
94+
@pytest.fixture
95+
def parser_aes192(mock_logger: MockLogger) -> DnsPacketParser:
96+
"""DnsPacketParser with AES-192-GCM (method 4)."""
97+
return DnsPacketParser(
98+
logger=mock_logger,
99+
encryption_key="testkey1234567890abcdef",
100+
encryption_method=4,
101+
)
102+
103+
104+
@pytest.fixture
105+
def parser_aes256(mock_logger: MockLogger) -> DnsPacketParser:
106+
"""DnsPacketParser with AES-256-GCM (method 5)."""
107+
return DnsPacketParser(
108+
logger=mock_logger,
109+
encryption_key="testkey1234567890abcdef01",
110+
encryption_method=5,
111+
)
112+
113+
114+
# ---------------------------------------------------------------------------
115+
# Temp file fixtures
116+
# ---------------------------------------------------------------------------
117+
118+
119+
@pytest.fixture
120+
def tmp_dir(tmp_path: Any) -> str:
121+
return str(tmp_path)
122+
123+
124+
@pytest.fixture
125+
def tmp_toml_file(tmp_path: Any) -> str:
126+
"""Write a minimal valid TOML config and return the path."""
127+
content = """
128+
[server]
129+
host = "127.0.0.1"
130+
port = 53
131+
132+
[logging]
133+
level = "DEBUG"
134+
"""
135+
p = tmp_path / "test_config.toml"
136+
p.write_text(content, encoding="utf-8")
137+
return str(p)
138+
139+
140+
@pytest.fixture
141+
def invalid_toml_file(tmp_path: Any) -> str:
142+
"""Write an invalid TOML file and return the path."""
143+
p = tmp_path / "bad_config.toml"
144+
p.write_text("this is [not valid toml ]]", encoding="utf-8")
145+
return str(p)
146+
147+
148+
# ---------------------------------------------------------------------------
149+
# Asyncio mock reader/writer
150+
# ---------------------------------------------------------------------------
151+
152+
153+
def make_mock_writer() -> MagicMock:
154+
"""Create a mock asyncio StreamWriter."""
155+
writer = MagicMock()
156+
writer.write = MagicMock()
157+
writer.drain = AsyncMock()
158+
writer.close = MagicMock()
159+
writer.wait_closed = AsyncMock()
160+
writer.is_closing = MagicMock(return_value=False)
161+
writer.can_write_eof = MagicMock(return_value=False)
162+
writer.get_extra_info = MagicMock(return_value=None)
163+
return writer
164+
165+
166+
def make_mock_reader(data: bytes = b"") -> MagicMock:
167+
"""Create a mock asyncio StreamReader that yields data then EOF."""
168+
reader = MagicMock()
169+
chunks = [data] if data else []
170+
chunks.append(b"") # EOF sentinel
171+
172+
async def _read(n: int = -1) -> bytes:
173+
if chunks:
174+
return chunks.pop(0)
175+
return b""
176+
177+
reader.read = _read
178+
return reader
179+
180+
181+
@pytest.fixture
182+
def mock_writer() -> MagicMock:
183+
return make_mock_writer()
184+
185+
186+
@pytest.fixture
187+
def mock_reader() -> MagicMock:
188+
return make_mock_reader(b"test payload data")
189+
190+
191+
# ---------------------------------------------------------------------------
192+
# Mock socket fixture
193+
# ---------------------------------------------------------------------------
194+
195+
196+
@pytest.fixture
197+
def mock_udp_socket() -> MagicMock:
198+
"""Create a mock non-blocking UDP socket."""
199+
sock = MagicMock()
200+
sock.fileno = MagicMock(return_value=5)
201+
sock.setblocking = MagicMock()
202+
sock.sendto = MagicMock(return_value=10)
203+
sock.recvfrom = MagicMock(return_value=(b"response", ("127.0.0.1", 53)))
204+
return sock
205+
206+
207+
# ---------------------------------------------------------------------------
208+
# Event loop fixture override (ensure clean loop per test)
209+
# ---------------------------------------------------------------------------
210+
211+
212+
@pytest.fixture
213+
def event_loop():
214+
"""Create a new event loop for each test."""
215+
loop = asyncio.new_event_loop()
216+
yield loop
217+
loop.close()

0 commit comments

Comments
 (0)