Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions server/recceiver/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
except (ConfigParser.NoOptionError, ValueError):
return D

def getint(self, key, D=None):

Check warning on line 53 in server/recceiver/processors.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this parameter "D" to match the regular expression ^[_a-z][a-z0-9_]*$.

See more on https://sonarcloud.io/project/issues?id=ChannelFinder_recsync&issues=AZ34NLfKkuuUEBEjZqIJ&open=AZ34NLfKkuuUEBEjZqIJ&pullRequest=148
try:
return self._C.getint(self._S, key, vars=self.env_vars)
except (ConfigParser.NoOptionError, ValueError):
return D

def __getitem__(self, key):
result = self.get(key)
if result is None:
Expand Down
1 change: 1 addition & 0 deletions server/sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sonar.python.version=3.9
52 changes: 52 additions & 0 deletions server/tests/test_cfstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from configparser import ConfigParser

from recceiver.cfstore import CFConfig
from recceiver.processors import ConfigAdapter


def make_adapter(section: str = "cf", values: dict = None, env: dict = None) -> ConfigAdapter:
parser = ConfigParser()
parser.add_section(section)
for key, value in (values or {}).items():
parser.set(section, key, str(value))
adapter = ConfigAdapter(parser, section)
if env:
adapter.env_vars = env
return adapter


class TestCFConfigLoads:
def test_loads_defaults_without_error(self):
adapter = make_adapter()
config = CFConfig.loads(adapter)
assert isinstance(config, CFConfig)

def test_default_push_max_retries(self):
adapter = make_adapter()
config = CFConfig.loads(adapter)
assert config.push_max_retries == 10

def test_push_max_retries_from_config(self):
adapter = make_adapter(values={"pushmaxretries": "3"})
config = CFConfig.loads(adapter)
assert config.push_max_retries == 3

def test_push_max_retries_from_env(self):
adapter = make_adapter(env={"pushmaxretries": "7"})
config = CFConfig.loads(adapter)
assert config.push_max_retries == 7

def test_default_push_always_retry(self):
adapter = make_adapter()
config = CFConfig.loads(adapter)
assert config.push_always_retry is True

def test_alias_disabled_by_default(self):
adapter = make_adapter()
config = CFConfig.loads(adapter)
assert config.alias_enabled is False

def test_alias_enabled_from_config(self):
adapter = make_adapter(values={"alias": "true"})
config = CFConfig.loads(adapter)
assert config.alias_enabled is True
46 changes: 46 additions & 0 deletions server/tests/test_container_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import time
from pathlib import Path

import pytest

from docker import DockerClient

SERVER_DIR = Path(__file__).parent.parent


@pytest.fixture(scope="session")
def recceiver_image():
client = DockerClient()
image, _ = client.images.build(path=str(SERVER_DIR), tag="recceiver-test:smoke", rm=True)
yield image.id
client.images.remove(image.id, force=True)


class TestContainerSmoke:
def test_container_starts_and_parses_config(self, recceiver_image):
"""Verify the container reaches the CF connection attempt rather than crashing during config parsing.

A config parsing error (e.g. AttributeError on a missing ConfigAdapter method) causes the
container to exit before CF_START is logged. A healthy startup logs CF_START first and only
then fails to connect to CF.
"""
client = DockerClient()
container = client.containers.run(
recceiver_image,
environment={
"RECCEIVER_RECCEIVER_PROCS": "cf",
"RECCEIVER_RECCEIVER_LOGLEVEL": "INFO",
"RECCEIVER_CF_BASEURL": "https://nonexistent-cf:8080/ChannelFinder",
"RECCEIVER_CF_CFUSERNAME": "admin",
"RECCEIVER_CF_CFPASSWORD": "password",
},
detach=True,
)
try:
time.sleep(10)
logs = container.logs().decode()
assert "AttributeError" not in logs, f"Container crashed with AttributeError:\n{logs}"
assert "CF_START" in logs, f"CF_START not found in logs — service may not have started:\n{logs}"
finally:
container.stop(timeout=5)
container.remove()
99 changes: 99 additions & 0 deletions server/tests/test_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import textwrap
Comment thread
anderslindho marked this conversation as resolved.
from configparser import ConfigParser
from pathlib import Path

from recceiver.cfstore import CFProcessor
from recceiver.processors import ConfigAdapter, ProcessorController


def make_adapter(section: str = "cf", values: dict = None, env: dict = None) -> ConfigAdapter:
parser = ConfigParser()
parser.add_section(section)
for key, value in (values or {}).items():
parser.set(section, key, str(value))
adapter = ConfigAdapter(parser, section)
if env:
adapter.env_vars = env
return adapter


class TestConfigAdapterGet:
def test_returns_value(self):
adapter = make_adapter(values={"baseurl": "https://cf:8080"})
assert adapter.get("baseurl") == "https://cf:8080"

def test_returns_default_when_missing(self):
adapter = make_adapter()
assert adapter.get("baseurl", "fallback") == "fallback"

def test_returns_none_when_missing_and_no_default(self):
adapter = make_adapter()
assert adapter.get("baseurl") is None

def test_env_var_overrides_config(self):
adapter = make_adapter(values={"baseurl": "https://original"}, env={"baseurl": "https://override"})
assert adapter.get("baseurl") == "https://override"

def test_env_var_provides_value_not_in_config(self):
adapter = make_adapter(env={"baseurl": "https://from-env"})
assert adapter.get("baseurl") == "https://from-env"


class TestConfigAdapterGetBoolean:
def test_returns_true(self):
adapter = make_adapter(values={"alias": "true"})
assert adapter.getboolean("alias") is True

def test_returns_false(self):
adapter = make_adapter(values={"alias": "false"})
assert adapter.getboolean("alias") is False

def test_returns_default_when_missing(self):
adapter = make_adapter()
assert adapter.getboolean("alias", False) is False

def test_returns_default_on_invalid_value(self):
adapter = make_adapter(values={"alias": "notabool"})
assert adapter.getboolean("alias", True) is True


class TestConfigAdapterGetInt:
def test_returns_int(self):
adapter = make_adapter(values={"pushmaxretries": "5"})
assert adapter.getint("pushmaxretries") == 5

def test_returns_default_when_missing(self):
adapter = make_adapter()
assert adapter.getint("pushmaxretries", 10) == 10

def test_returns_none_when_missing_and_no_default(self):
adapter = make_adapter()
assert adapter.getint("pushmaxretries") is None

def test_returns_default_on_invalid_value(self):
adapter = make_adapter(values={"pushmaxretries": "notanint"})
assert adapter.getint("pushmaxretries", 10) == 10

def test_env_var_overrides_config(self):
adapter = make_adapter(values={"pushmaxretries": "5"}, env={"pushmaxretries": "99"})
assert adapter.getint("pushmaxretries", 10) == 99


class TestProcessorControllerFromConfig:
def test_cf_processor_initialized_from_config_file(self, tmp_path: Path):
config_file = tmp_path / "recceiver.conf"
config_file.write_text(
textwrap.dedent(
"""\
[recceiver]
procs = cf

[cf]
pushMaxRetries = 5
"""
)
Comment thread
anderslindho marked this conversation as resolved.
)
ctrl = ProcessorController(cfile=str(config_file))
assert len(ctrl.procs) == 1
assert isinstance(ctrl.procs[0], CFProcessor)
assert ctrl.procs[0].cf_config.push_max_retries == 5
Loading