Skip to content
This repository was archived by the owner on Dec 30, 2025. It is now read-only.

Commit 5f49d24

Browse files
committed
feat: enhance testing framework with Docker support and new fixtures
- Introduced Docker fixtures using pytest-docker-tools for improved container management during tests. - Added a `prepared_config_dir` fixture to create and manage UnrealIRCd configuration files for testing. - Updated controller and base controller classes to support Docker container integration. - Enhanced test cases to utilize the new Docker fixtures, improving isolation and reliability of tests. - Refactored existing tests to leverage the new controller setup, ensuring better organization and maintainability. - Cleaned up and optimized various test files for consistency and clarity.
1 parent f20b32b commit 5f49d24

27 files changed

+1095
-753
lines changed

tests/conftest.py

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,78 @@
77
"""
88

99
import importlib
10-
import pytest
11-
import docker
12-
import requests
1310
import os
1411
import time
1512
from pathlib import Path
16-
from typing import Generator, Optional, Type
1713

18-
from .utils.base_test_cases import BaseServerTestCase
14+
import docker
15+
import pytest
16+
17+
# Docker fixtures using pytest-docker-tools
18+
from pytest_docker_tools import container, image
19+
1920
from .controllers.base_controllers import BaseServerController, TestCaseControllerConfig
21+
from .utils.base_test_cases import BaseServerTestCase
22+
23+
unrealircd_image = image(name="ircatlchat-unrealircd:latest")
24+
25+
26+
@pytest.fixture(scope="function")
27+
def prepared_config_dir(tmp_path):
28+
"""Create and prepare a temporary directory with UnrealIRCd config files."""
29+
import shutil
30+
from pathlib import Path
31+
32+
config_dir = tmp_path / "container_config"
33+
config_dir.mkdir(exist_ok=True)
34+
35+
# Source directory with real configs
36+
source_dir = Path("src/backend/unrealircd/conf")
37+
38+
# Copy all config files
39+
for config_file in source_dir.glob("*.conf"):
40+
dest_file = config_dir / config_file.name
41+
shutil.copy2(config_file, dest_file)
42+
dest_file.chmod(0o644) # Make readable by anyone
43+
44+
# Copy subdirectories
45+
for subdir in ["help", "aliases", "tls"]:
46+
if (source_dir / subdir).exists():
47+
shutil.copytree(source_dir / subdir, config_dir / subdir, dirs_exist_ok=True)
48+
# Set permissions on subdirectory files
49+
for file in (config_dir / subdir).rglob("*"):
50+
if file.is_file():
51+
file.chmod(0o644)
52+
53+
# Copy other necessary files
54+
for pattern in ["*.list", "*.default.conf", "*.optional.conf"]:
55+
for file in source_dir.glob(pattern):
56+
dest_file = config_dir / file.name
57+
shutil.copy2(file, dest_file)
58+
dest_file.chmod(0o644)
59+
60+
print(f"DEBUG: Prepared config dir: {config_dir}")
61+
print(f"DEBUG: Files in config dir: {list(config_dir.glob('*'))}")
62+
print(f"DEBUG: unrealircd.conf exists: {(config_dir / 'unrealircd.conf').exists()}")
63+
64+
return config_dir
65+
66+
67+
unrealircd_container = container(
68+
image="{unrealircd_image.id}",
69+
ports={
70+
"6667/tcp": None, # Main IRC port
71+
"6697/tcp": None, # TLS port
72+
},
73+
volumes={
74+
"{prepared_config_dir}": {"bind": "/home/unrealircd/unrealircd/conf", "mode": "rw"},
75+
},
76+
command=[
77+
"-t", # Test configuration
78+
"-F", # Don't fork
79+
],
80+
scope="function",
81+
)
2082

2183

2284
def pytest_addoption(parser):
@@ -42,22 +104,22 @@ def pytest_configure(config):
42104
try:
43105
module = importlib.import_module(module_name)
44106
except ImportError:
45-
pytest.exit("Cannot import module {}".format(module_name), 1)
107+
pytest.exit(f"Cannot import module {module_name}", 1)
46108

47109
controller_class = module.get_irctest_controller_class()
48110
if issubclass(controller_class, BaseServerController):
49111
from . import server_tests as module
50112
else:
51113
pytest.exit(
52-
"{}.Controller should be a subclass of irctest.basecontroller.BaseServerController".format(module_name),
114+
f"{module_name}.Controller should be a subclass of irctest.basecontroller.BaseServerController",
53115
1,
54116
)
55117

56118
if services_module_name is not None:
57119
try:
58120
services_module = importlib.import_module(services_module_name)
59121
except ImportError:
60-
pytest.exit("Cannot import module {}".format(services_module_name), 1)
122+
pytest.exit(f"Cannot import module {services_module_name}", 1)
61123
controller_class.services_controller_class = services_module.get_irctest_controller_class()
62124

63125
BaseServerTestCase.controllerClass = controller_class
@@ -177,6 +239,19 @@ def mock_docker_container(mocker):
177239
return mock_container
178240

179241

242+
@pytest.fixture
243+
def controller(unrealircd_container):
244+
"""Controller instance with Docker container support."""
245+
from .controllers.unrealircd_controller import get_unrealircd_controller_class
246+
247+
controller_class = get_unrealircd_controller_class()
248+
config = TestCaseControllerConfig()
249+
return controller_class(config, container_fixture=unrealircd_container)
250+
251+
252+
# Removed autouse controller injection - tests should explicitly request controller fixture when needed
253+
254+
180255
@pytest.fixture
181256
def mock_requests_get(mocker):
182257
"""Mock requests.get for testing HTTP calls."""
@@ -204,6 +279,7 @@ def is_service_running(self, service_name: str) -> bool:
204279

205280
result = subprocess.run(
206281
["docker", "compose", "ps", service_name],
282+
check=False,
207283
cwd=self.project_root,
208284
capture_output=True,
209285
text=True,
@@ -219,6 +295,7 @@ def get_service_logs(self, service_name: str, tail: int = 50) -> str:
219295

220296
result = subprocess.run(
221297
["docker", "compose", "logs", "--tail", str(tail), service_name],
298+
check=False,
222299
cwd=self.project_root,
223300
capture_output=True,
224301
text=True,
@@ -264,7 +341,7 @@ def wait_for_irc_server(self, timeout: int = 30) -> bool:
264341

265342
return False
266343

267-
def send_irc_command(self, command: str) -> Optional[str]:
344+
def send_irc_command(self, command: str) -> str | None:
268345
"""Send a command to the IRC server and get response."""
269346
import socket
270347

@@ -399,3 +476,23 @@ def setup_test_environment(project_root: Path, tmp_path_factory):
399476
test_env_vars = ["TESTING", "DOCKER_COMPOSE_FILE"]
400477
for var in test_env_vars:
401478
os.environ.pop(var, None)
479+
480+
481+
def _inject_controller_if_needed(request):
482+
"""Helper to inject controller only when needed."""
483+
if hasattr(request.instance, "setup_method"):
484+
# Check if this is an integration test that needs Docker
485+
if any(marker in ["integration", "irc", "docker", "atheme", "webpanel"] for marker in request.keywords):
486+
# Only request controller fixture when actually needed
487+
controller = request.getfixturevalue("controller")
488+
request.instance.controller = controller
489+
# Set up connection details
490+
container_ports = controller.get_container_ports()
491+
request.instance.hostname = "localhost"
492+
request.instance.port = container_ports.get("6667/tcp", 6667)
493+
494+
495+
@pytest.fixture(autouse=True)
496+
def inject_controller(request):
497+
"""Automatically inject controller fixture into test classes that need it."""
498+
_inject_controller_if_needed(request)

tests/controllers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
from .atheme_controller import get_atheme_controller_class
88
from .base_controllers import (
99
BaseController,
10-
DirectoryBasedController,
1110
BaseServerController,
1211
BaseServicesController,
12+
DirectoryBasedController,
1313
ProcessStopped,
1414
)
1515
from .unrealircd_controller import get_unrealircd_controller_class
1616

1717
__all__ = [
1818
"BaseController",
19-
"DirectoryBasedController",
2019
"BaseServerController",
2120
"BaseServicesController",
21+
"DirectoryBasedController",
2222
"ProcessStopped",
2323
"get_atheme_controller_class",
2424
"get_unrealircd_controller_class",

tests/controllers/atheme_controller.py

Lines changed: 24 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,12 @@
33
Adapted from irctest's Atheme controller for our testing infrastructure.
44
"""
55

6-
from typing import Optional
6+
import shutil
7+
from pathlib import Path
78

89
from .base_controllers import BaseServicesController, DirectoryBasedController
910

1011

11-
TEMPLATE_CONFIG = """
12-
me {{
13-
name "My.Little.Services";
14-
sid "001";
15-
description "test services";
16-
uplink "My.Little.Server";
17-
}}
18-
19-
numeric "001";
20-
21-
connpass {{
22-
password "password";
23-
}}
24-
25-
log {{
26-
method {{
27-
file {{
28-
filename "{log_file}";
29-
}}
30-
}}
31-
level {{
32-
all;
33-
}}
34-
}}
35-
36-
modules {{
37-
load "modules/protocol/unreal4";
38-
load "modules/backend/file";
39-
load "modules/crypto/pbkdf2";
40-
load "modules/crypto/scram-sha";
41-
}}
42-
43-
database {{
44-
type "file";
45-
name "{db_file}";
46-
}}
47-
48-
nickserv {{
49-
guestnick "Guest";
50-
}}
51-
52-
chanserv {{
53-
maxchans "100";
54-
}}
55-
56-
operserv {{
57-
autokill "30";
58-
akilltime "30";
59-
}}
60-
61-
global {{
62-
language "en";
63-
}}
64-
65-
serverinfo {{
66-
name "My.Little.Services";
67-
description "test services";
68-
numeric "001";
69-
reconnect "10";
70-
netname "ExampleNET";
71-
}}
72-
73-
ulines {{
74-
"My.Little.Services";
75-
}}
76-
"""
77-
78-
7912
class AthemeController(BaseServicesController, DirectoryBasedController):
8013
"""Controller for managing Atheme services during testing."""
8114

@@ -85,10 +18,20 @@ def __init__(self, *args, **kwargs):
8518
self.services_port: int | None = None
8619

8720
def create_config(self) -> None:
88-
"""Create the configuration directory and basic files."""
21+
"""Create the configuration directory and copy real config files."""
8922
super().create_config()
23+
9024
if self.directory:
91-
(self.directory / "atheme.conf").touch()
25+
# Source directory with real configs
26+
source_dir = Path(__file__).parent.parent.parent / "src" / "backend" / "atheme" / "conf"
27+
28+
# Copy all config files (just like UnrealIRCd does)
29+
for config_file in source_dir.glob("*.conf"):
30+
shutil.copy2(config_file, self.directory / config_file.name)
31+
32+
# For testing, we need to modify the uplink configuration
33+
# The production config has hardcoded values, but tests need dynamic ones
34+
# We'll update it in the run() method with the correct hostname/port
9235

9336
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
9437
"""Start the Atheme services."""
@@ -100,28 +43,24 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
10043
self.create_config()
10144

10245
if self.directory:
103-
# Create log and database files
104-
log_file = self.directory / "atheme.log"
105-
db_file = self.directory / "services.db"
46+
config_file = self.directory / "atheme.conf"
10647

107-
config_content = TEMPLATE_CONFIG.format(
108-
log_file=log_file,
109-
db_file=db_file,
110-
)
48+
# Update the config file with the correct server connection details
49+
if config_file.exists():
50+
content = config_file.read_text()
51+
# Replace the uplink configuration with test values
52+
import re
11153

112-
config_file = self.directory / "atheme.conf"
113-
with config_file.open("w") as f:
114-
f.write(config_content)
54+
content = re.sub(r'uplink "[^"]*" \{', 'uplink "test.server" {', content)
55+
content = re.sub(r'host = "[^"]*";', f'host = "{server_hostname}";', content)
56+
content = re.sub(r"port = \d+;", f"port = {server_port};", content)
57+
config_file.write_text(content)
11558

11659
self.proc = self.execute(
11760
[
11861
"atheme-services",
11962
"-f",
12063
config_file,
121-
"-p",
122-
str(server_port),
123-
"-h",
124-
server_hostname,
12564
]
12665
)
12766

0 commit comments

Comments
 (0)