mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-07-28 22:39:26 +00:00
feat(config, validation): enhance config validation with strict mode and update example checks
- Added a `--strict` option to `validate_config.py` to fail on "Unknown section" messages, aimed at CI validation of example configs. - Updated GitHub Actions workflow to validate shipped example configs against the canonical section list using the new strict mode. - Expanded the `CANONICAL_NON_COMMAND_SECTIONS` in `config_validation.py` to include additional sections for improved validation accuracy. - Introduced a regression test to ensure example configs do not trigger unknown section warnings, maintaining consistency with the canonical list.
This commit is contained in:
@@ -75,6 +75,16 @@ jobs:
|
||||
- name: Log injection check — fail on new unsanitized logger calls
|
||||
run: python scripts/check_log_injection.py
|
||||
|
||||
- name: Config validation — fail on unrecognized sections in shipped configs
|
||||
run: |
|
||||
set -e
|
||||
for f in config.ini.example config.ini.minimal-example config.ini.quickstart config.min.ini config-pymc.ini; do
|
||||
if [ -f "$f" ]; then
|
||||
echo "Validating $f"
|
||||
python validate_config.py --config "$f" --strict
|
||||
fi
|
||||
done
|
||||
|
||||
typecheck:
|
||||
name: Type check (mypy)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -39,6 +39,7 @@ def _channel_name_is_public(name: str) -> bool:
|
||||
CANONICAL_NON_COMMAND_SECTIONS = frozenset({
|
||||
"Connection",
|
||||
"Bot",
|
||||
"Admin",
|
||||
"Channels",
|
||||
"Banned_Users",
|
||||
"Localization",
|
||||
@@ -46,6 +47,7 @@ CANONICAL_NON_COMMAND_SECTIONS = frozenset({
|
||||
"Plugin_Overrides",
|
||||
"Companion_Purge",
|
||||
"Keywords",
|
||||
"RandomLine",
|
||||
"Scheduled_Messages",
|
||||
"Logging",
|
||||
"Custom_Syntax",
|
||||
@@ -53,12 +55,21 @@ CANONICAL_NON_COMMAND_SECTIONS = frozenset({
|
||||
"Weather",
|
||||
"Solar_Config",
|
||||
"Channels_List",
|
||||
"Data_Retention",
|
||||
"Web_Viewer",
|
||||
"Feed_Manager",
|
||||
"PacketCapture",
|
||||
"MapUploader",
|
||||
"Weather_Service",
|
||||
"MqttWeather",
|
||||
"Earthquake_Service",
|
||||
"Worldcup_Service",
|
||||
"Rate_Limits",
|
||||
"Webhook",
|
||||
"RepeaterPrefixCollision_Service",
|
||||
"DiscordBridge",
|
||||
"TelegramBridge",
|
||||
"DARC_MoWaS_Service",
|
||||
})
|
||||
|
||||
# Sections required for the bot to start (accessed without has_section guards)
|
||||
|
||||
@@ -176,6 +176,30 @@ test = ack
|
||||
)
|
||||
|
||||
|
||||
class TestExampleConfigsHaveNoUnknownSections:
|
||||
"""Regression test for #215: shipped example configs must not trigger
|
||||
'Unknown section' info messages, i.e. CANONICAL_NON_COMMAND_SECTIONS must
|
||||
stay in sync with the sections actually used in the example files."""
|
||||
|
||||
def test_example_configs_have_no_unknown_sections(self):
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
example_files = [
|
||||
"config.ini.example",
|
||||
"config.ini.minimal-example",
|
||||
"config.ini.quickstart",
|
||||
]
|
||||
for filename in example_files:
|
||||
path = base / filename
|
||||
if not path.exists():
|
||||
continue
|
||||
results = validate_config(str(path))
|
||||
unknown = [
|
||||
r for r in results
|
||||
if r[0] == SEVERITY_INFO and "not in canonical list" in r[1]
|
||||
]
|
||||
assert unknown == [], f"{filename} has unrecognized sections: {unknown}"
|
||||
|
||||
|
||||
class TestPathValidation:
|
||||
"""Tests for path writability validation."""
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Tests for the validate_config.py CLI entry point, in particular --strict."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
VALID_CONFIG = """[Connection]
|
||||
connection_type = serial
|
||||
serial_port = /dev/ttyUSB0
|
||||
|
||||
[Bot]
|
||||
bot_name = TestBot
|
||||
db_path = {db_path}
|
||||
|
||||
[Channels]
|
||||
monitor_channels = general
|
||||
respond_to_dms = true
|
||||
|
||||
[Keywords]
|
||||
test = ack
|
||||
"""
|
||||
|
||||
CONFIG_WITH_UNKNOWN_SECTION = VALID_CONFIG + """
|
||||
[TotallyMadeUpSection]
|
||||
foo = bar
|
||||
"""
|
||||
|
||||
|
||||
def _run_validate_config(config_path: Path, *extra_args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, "validate_config.py", "--config", str(config_path), *extra_args],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
class TestValidateConfigCli:
|
||||
def test_unknown_section_does_not_fail_without_strict(self, tmp_path):
|
||||
config = tmp_path / "config.ini"
|
||||
config.write_text(CONFIG_WITH_UNKNOWN_SECTION.format(db_path=str(tmp_path / "meshcore_bot.db")))
|
||||
result = _run_validate_config(config)
|
||||
assert result.returncode == 0
|
||||
assert "Unknown section [TotallyMadeUpSection]" in result.stderr
|
||||
|
||||
def test_unknown_section_fails_with_strict(self, tmp_path):
|
||||
config = tmp_path / "config.ini"
|
||||
config.write_text(CONFIG_WITH_UNKNOWN_SECTION.format(db_path=str(tmp_path / "meshcore_bot.db")))
|
||||
result = _run_validate_config(config, "--strict")
|
||||
assert result.returncode == 1
|
||||
assert "Unknown section [TotallyMadeUpSection]" in result.stderr
|
||||
|
||||
def test_valid_config_passes_with_strict(self, tmp_path):
|
||||
config = tmp_path / "config.ini"
|
||||
config.write_text(VALID_CONFIG.format(db_path=str(tmp_path / "meshcore_bot.db")))
|
||||
result = _run_validate_config(config, "--strict")
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_error_still_fails_regardless_of_strict(self, tmp_path):
|
||||
config = tmp_path / "config.ini"
|
||||
config.write_text("[Bot]\nbot_name = Test\n")
|
||||
result = _run_validate_config(config)
|
||||
assert result.returncode == 1
|
||||
@@ -4,6 +4,12 @@ Validate MeshCore Bot config.ini section names.
|
||||
|
||||
Run standalone: python validate_config.py [--config config.ini]
|
||||
Exits with 1 if any errors are found, 0 otherwise. Warnings and info are printed but do not affect exit code.
|
||||
|
||||
Use --strict to also fail on "Unknown section" info messages. This is intended for
|
||||
CI checks against the shipped example configs (config.ini.example, etc.), which should
|
||||
never contain a section outside the canonical list (see CANONICAL_NON_COMMAND_SECTIONS
|
||||
in modules/config_validation.py). It is not recommended for validating end-user configs,
|
||||
since those may legitimately contain sections for local/third-party plugins.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -25,6 +31,15 @@ def main() -> int:
|
||||
default="config.ini",
|
||||
help="Path to configuration file (default: config.ini)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Also fail (exit 1) on 'Unknown section' info messages. Intended for "
|
||||
"CI validation of the shipped example configs; not recommended for "
|
||||
"end-user configs, which may have local/third-party plugin sections."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
results = validate_config(args.config)
|
||||
@@ -37,6 +52,8 @@ def main() -> int:
|
||||
print(f"Warning: {message}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Info: {message}", file=sys.stderr)
|
||||
if args.strict and "Unknown section" in message:
|
||||
has_error = True
|
||||
|
||||
return 1 if has_error else 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user