mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 11:57:16 +00:00
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.
58 lines
2.0 KiB
Python
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
|