Files
meshcore-bot/tests/unit/test_weather_blitz_area.py
agessaman 7758403148 feat(web_viewer): cover every plugin setting, with a template reference and preview
The Plugins page now has a field for every setting the plugins read. I compared each plugin's settings_schema against config.ini.example and the keys the code reads; fourteen plugins had gaps (greeter, trace, stats, path, wx/gwx, rain, test, multitest, sports, worldcup, cmd, feed, the webhook, weather, packet capture, earthquake and prefix-collision services) and the World Cup service had no form at all. Webhook URLs, bot tokens and channel keys use the password field. A guard test needed four [Rain_Command] keys documented in config.ini.example.

Template fields get a { } button that opens a reference of the placeholders they take, with click-to-insert. Fields rendered by the piped engine (test's response_format, path's reply_prefix) also list the filters and conditionals. Every template field previews as you type: piped fields against a 2-byte, a 1-byte and a direct sample message, so a conditional's effect shows before saving, and the str.format fields (greeter, mesh info, multitest, MQTT weather) as their plugin renders them, including what it sends when the template has a mistake. The specs and renderers live in modules/template_reference.py; the preview endpoint only renders and never saves. The greeter's channel_greetings parser and multi-part split moved into module functions so the command and the preview share them.

Fixes the new fields would otherwise have exposed:
- Clearing a number field saved `key =`, which makes getint/getfloat raise and the plugin fail to load. A cleared number now removes the key.
- The weather service raised in __init__ when only some blitz_area_* corners were set. It now needs all four and logs a warning otherwise.
- A blank MQTT weather json_template saved an empty template, so every reply said no message had arrived. Blank now saves the default.
2026-10-04 15:41:45 -07:00

58 lines
2.0 KiB
Python

#!/usr/bin/env python3
"""Unit tests for reading the [Weather_Service] blitz_area_* lightning box."""
import configparser
from unittest.mock import AsyncMock, Mock
from modules.service_plugins.weather_service import WeatherService
BOX = {
"blitz_area_min_lat": "47.0",
"blitz_area_min_lon": "-123.0",
"blitz_area_max_lat": "48.5",
"blitz_area_max_lon": "-121.5",
}
def _weather_service(mock_logger, extra_cfg=None):
config = configparser.ConfigParser()
config.add_section("Weather")
config.add_section("Weather_Service")
config.set("Weather_Service", "my_position_lat", "47.6062")
config.set("Weather_Service", "my_position_lon", "-122.3321")
for key, val in (extra_cfg or {}).items():
config.set("Weather_Service", key, val)
bot = Mock()
bot.logger = mock_logger
bot.config = config
bot.db_manager = Mock()
bot.command_manager = Mock()
bot.command_manager.send_channel_message = AsyncMock()
return WeatherService(bot)
class TestBlitzArea:
def test_unset_means_off(self, mock_logger):
assert _weather_service(mock_logger).blitz_area is None
def test_all_four_corners(self, mock_logger):
svc = _weather_service(mock_logger, BOX)
assert svc.blitz_area == {
"min_lat": 47.0, "min_lon": -123.0, "max_lat": 48.5, "max_lon": -121.5,
}
def test_partial_box_is_off_instead_of_failing_init(self, mock_logger):
"""The web viewer saves one corner at a time; a half-filled box used to
raise NoOptionError and take the whole service down."""
svc = _weather_service(mock_logger, {"blitz_area_min_lat": "47.0"})
assert svc.blitz_area is None
mock_logger.warning.assert_any_call(
"Lightning detection off: blitz_area_min_lat, blitz_area_min_lon, "
"blitz_area_max_lat and blitz_area_max_lon must all be numbers"
)
def test_blank_corners_are_unset(self, mock_logger):
svc = _weather_service(mock_logger, dict.fromkeys(BOX, ""))
assert svc.blitz_area is None