Files
LEDMatrix/test/web_interface/test_dotted_league_keys.py
Ron Pierce ab0cfd2362 fix(web): preserve dotted schema keys when saving plugin config (#370)
The plugin config form posts form-data with dot-notation paths
(e.g. "leagues.fifa.world.enabled"). _get_schema_property and
_set_nested_value split those paths on every dot, so a schema key that
itself contains a dot (soccer league keys like "fifa.world", "eng.1")
was mistaken for nested "fifa" -> "world" objects. Per-league edits
(enable, favorite_teams, nested booleans) were written to a fabricated
"leagues.fifa.world" branch while the real league object was never
updated, so saves silently dropped the change and produced a
byte-identical config.

Both helpers now greedily match the longest path segment that exists in
the schema (_get_schema_property) or the config being updated
(_set_nested_value), mirroring the frontend's dotted-key handling.

Adds regression tests covering schema lookup, value typing, and writes
under dotted league keys, plus a guard that plain nested paths still work.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:06:32 -04:00

94 lines
3.6 KiB
Python

"""
Regression test for saving plugin config fields whose schema keys contain dots
(e.g. soccer league keys like "fifa.world", "eng.1", "usa.1").
Bug: the web config form posts form-data with dotted paths such as
"leagues.fifa.world.enabled". The helpers that resolve those paths split on every
dot, so the dotted league key "fifa.world" was mistaken for nested "fifa" ->
"world" objects. Per-league edits (enable, favorite_teams, nested booleans) were
written to a fabricated "leagues.fifa.world" branch while the real league object
was never updated, so the save silently dropped the change and the saved config
came out byte-identical.
"""
import unittest
from web_interface.blueprints.api_v3 import (
_get_schema_property,
_set_nested_value,
_parse_form_value_with_schema,
)
SCHEMA = {
"type": "object",
"properties": {
"leagues": {
"type": "object",
"properties": {
"fifa.world": {
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"favorite_teams": {
"type": "array",
"items": {"type": "string"},
},
"display_modes": {
"type": "object",
"properties": {"live": {"type": "boolean"}},
},
},
}
},
}
},
}
class TestDottedLeagueKeys(unittest.TestCase):
def test_schema_lookup_resolves_dotted_league_key(self):
prop = _get_schema_property(SCHEMA, "leagues.fifa.world.favorite_teams")
self.assertIsNotNone(prop, "dotted league key path should resolve")
self.assertEqual(prop.get("type"), "array")
def test_schema_lookup_resolves_nested_object_beneath_dotted_key(self):
live = _get_schema_property(SCHEMA, "leagues.fifa.world.display_modes.live")
self.assertIsNotNone(live)
self.assertEqual(live.get("type"), "boolean")
def test_parse_typed_value_for_dotted_key(self):
# Comma-separated text input "USA" must become an array, not the raw string.
parsed = _parse_form_value_with_schema(
"USA", "leagues.fifa.world.favorite_teams", SCHEMA
)
self.assertEqual(parsed, ["USA"])
def test_set_value_updates_real_league_not_fabricated_branch(self):
config = {"leagues": {"fifa.world": {"enabled": False, "favorite_teams": []}}}
_set_nested_value(config, "leagues.fifa.world.enabled", True)
_set_nested_value(config, "leagues.fifa.world.favorite_teams", ["USA"])
self.assertTrue(config["leagues"]["fifa.world"]["enabled"])
self.assertEqual(config["leagues"]["fifa.world"]["favorite_teams"], ["USA"])
# The real league must be updated and no fabricated "fifa" branch created.
self.assertNotIn("fifa", config["leagues"])
def test_set_value_into_missing_leaf_lands_in_real_league(self):
# A leaf that does not exist yet still resolves into the real dotted league.
config = {"leagues": {"fifa.world": {"enabled": False}}}
_set_nested_value(config, "leagues.fifa.world.display_modes.live", True)
self.assertTrue(
config["leagues"]["fifa.world"]["display_modes"]["live"]
)
self.assertNotIn("fifa", config["leagues"])
def test_plain_nested_paths_still_work(self):
config = {}
_set_nested_value(config, "customization.text.font", "small")
self.assertEqual(config["customization"]["text"]["font"], "small")
if __name__ == "__main__":
unittest.main()