mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-07 19:58:08 +00:00
- first_time_install.sh: verify chown/chmod succeed and the final owner/group/mode on config_secrets.json before reporting success; exit with a clear error otherwise instead of swallowing failures. - api_v3.py: replace the predictable .writetest probe with an exclusive NamedTemporaryFile to avoid a race with concurrent resolvers; log the preferred/fallback export path and OSError when falling back to the reinstall-deleted directory. - api_v3.py: mark a restore as failed when plugin reinstalls fail, even if file restoration itself succeeded, so the endpoint no longer reports HTTP 200 success on a partial restore. - api_v3.py: stringify plugin IDs before joining them into the error message so a malformed backup's non-string plugin_id can't raise a TypeError and mask the detailed response. - backup_manager.py / api_v3.py: stop putting raw exception text (originating from a user-controlled backup file) into restore results returned to the client; log full details server-side instead. Addresses the CodeQL "stack trace information exposure" alert. - test coverage: add a test for get_plugin_info() resolving a manifest id, and assert the disabled restore_wifi path also skips and omits ytm_auth.json.
115 lines
4.7 KiB
Python
115 lines
4.7 KiB
Python
"""A plugin must be findable in the registry by the id it calls itself.
|
|
|
|
Four shipped plugins have a registry ``id`` that differs from the ``id`` in
|
|
their own ``manifest.json``:
|
|
|
|
directory / manifest.json id registry id
|
|
ledmatrix-weather weather
|
|
ledmatrix-stocks stocks
|
|
ledmatrix-music music
|
|
ledmatrix-leaderboard leaderboard
|
|
|
|
The installer already knows about this: it deliberately names the install
|
|
directory after the *manifest* id (store_manager, "Use manifest ID for
|
|
directory name"), and warns when the two disagree. So on disk, in
|
|
``config.json`` and in a backup manifest, these plugins are called
|
|
``ledmatrix-weather``. Only the registry calls them ``weather``.
|
|
|
|
Nothing resolved that in reverse. Asking the store to install
|
|
``ledmatrix-weather`` -- which is exactly what restoring a backup does --
|
|
failed with "Plugin not found in registry", and four enabled plugins went
|
|
missing from a restored device with no error surfaced to the user.
|
|
|
|
Renaming the registry ids would orphan existing ``plugin_state.json`` entries
|
|
keyed on the old ones, so the lookup resolves ``plugin_path`` instead: the
|
|
registry already records ``plugins/ledmatrix-weather``, which is unambiguous
|
|
and needs no published identity to change.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from src.plugin_system.store_manager import PluginStoreManager # noqa: E402
|
|
|
|
|
|
# Shaped like the real registry: id and plugin_path basename disagree for the
|
|
# first entry, agree for the second.
|
|
REGISTRY: Dict[str, List[Dict[str, Any]]] = {
|
|
"plugins": [
|
|
{
|
|
"id": "weather",
|
|
"name": "Weather",
|
|
"plugin_path": "plugins/ledmatrix-weather",
|
|
"repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
|
|
},
|
|
{
|
|
"id": "ledmatrix-flights",
|
|
"name": "Flights",
|
|
"plugin_path": "plugins/ledmatrix-flights",
|
|
"repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
|
|
},
|
|
{
|
|
"id": "third-party",
|
|
"name": "Third Party",
|
|
"plugin_path": "",
|
|
"repo": "https://github.com/someone/thing",
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def store(monkeypatch: pytest.MonkeyPatch) -> PluginStoreManager:
|
|
manager = PluginStoreManager.__new__(PluginStoreManager)
|
|
monkeypatch.setattr(manager, "fetch_registry", lambda *a, **k: REGISTRY, raising=False)
|
|
return manager
|
|
|
|
|
|
def _ids(entry: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
return entry.get("id") if entry else None
|
|
|
|
|
|
class TestRegistryLookupByManifestId:
|
|
def test_get_plugin_info_resolves_manifest_id(self, store: PluginStoreManager) -> None:
|
|
"""get_plugin_info() delegates to the same lookup as get_registry_info()."""
|
|
assert (
|
|
_ids(store.get_plugin_info("ledmatrix-weather", fetch_latest_from_github=False))
|
|
== "weather"
|
|
)
|
|
|
|
def test_exact_registry_id_still_resolves(self, store: PluginStoreManager) -> None:
|
|
assert _ids(store.get_registry_info("weather")) == "weather"
|
|
|
|
def test_manifest_id_resolves_via_plugin_path(self, store: PluginStoreManager) -> None:
|
|
"""The case that broke restore: asked by the name on disk."""
|
|
assert _ids(store.get_registry_info("ledmatrix-weather")) == "weather", (
|
|
"a plugin installed as 'ledmatrix-weather' could not be found in a "
|
|
"registry that lists it under plugin_path plugins/ledmatrix-weather")
|
|
|
|
def test_matching_id_and_path_unaffected(self, store: PluginStoreManager) -> None:
|
|
assert _ids(store.get_registry_info("ledmatrix-flights")) == "ledmatrix-flights"
|
|
|
|
def test_unknown_plugin_still_returns_none(self, store: PluginStoreManager) -> None:
|
|
assert store.get_registry_info("no-such-plugin") is None
|
|
|
|
def test_empty_plugin_path_is_not_a_wildcard(self, store: PluginStoreManager) -> None:
|
|
"""Third-party entries carry plugin_path "" — that must not match ""."""
|
|
assert store.get_registry_info("") is None
|
|
|
|
def test_exact_id_wins_over_a_path_match(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""If some other entry's path collides with a real id, id wins."""
|
|
registry = {
|
|
"plugins": [
|
|
{"id": "decoy", "plugin_path": "plugins/weather"},
|
|
{"id": "weather", "plugin_path": "plugins/ledmatrix-weather"},
|
|
]
|
|
}
|
|
manager = PluginStoreManager.__new__(PluginStoreManager)
|
|
monkeypatch.setattr(manager, "fetch_registry", lambda *a, **k: registry, raising=False)
|
|
assert _ids(manager.get_registry_info("weather")) == "weather"
|