velxio/test/backend/unit/test_micropython_libs.py

87 lines
2.8 KiB
Python
Raw Normal View History

feat(micropython): install libraries from the micropython-lib index (#214) A MicroPython library in Velxio IS a .py file in the project, copied onto the board's filesystem before main.py runs. That mechanism worked, but getting the file meant hunting GitHub by hand — the gap issue #214 is about. Now the Library Manager fills it, in both halves: Backend: /api/micropython-libs proxies the official micropython-lib package index (the same one `mip` uses on real hardware). /search filters the index; /fetch resolves a package plus its declared deps into .py files. The proxy is what makes this safe to expose: only the official index is reachable (the name must exist in index.json, files come from its content-addressed store), source only (.py, never .mpy bytecode), path- safe, and capped at 24 files / 256 KB per install. In-process TTL cache over the index. Frontend: the same Library Manager modal, mode-aware. On a MicroPython board the search hits the index and "Add to project" writes the package's files into the board's workspace (upsert — re-adding is the upgrade path), visible in the explorer and travelling with the project. Before typing it shows featured picks (ssd1306, umqtt.*, urequests) and the modules already frozen into the firmware (neopixel, dht, onewire, ds18x20, machine, network) labeled "built into the firmware", so searching for one answers "just import it" instead of an empty list. The file-materialization prelude now creates package directories: umqtt ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the prelude died with ENOENT on any packaged module. Also ships a gallery example ("ESP32 + MicroPython: use an external library") that documents the file-is-the-library mechanism with the ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
"""micropython-libs route (issue #214) — the pure validation layer.
The network calls are exercised end-to-end against the real index by the
deploy smoke; here we pin the rules that make the endpoint safe to expose:
source-only, path-safe, bounded, and honest search ordering.
"""
import pytest
from app.api.routes.micropython_libs import (
MAX_FILES_PER_PACKAGE,
filter_packages,
validate_package_files,
)
# ── validate_package_files ──────────────────────────────────────────────────
def test_accepts_plain_py_files():
out = validate_package_files(
[["ssd1306.py", "156dcec1"], ["umqtt/simple.py", "abc123"]], package="x"
)
assert out == [("ssd1306.py", "156dcec1"), ("umqtt/simple.py", "abc123")]
@pytest.mark.parametrize(
"path",
[
"driver.mpy", # bytecode — the user could not read what runs
"native.so",
"README.md",
"/etc/passwd.py", # absolute
"../evil.py", # traversal
"a/../../evil.py",
"a//b.py", # empty segment
"c:\\windows\\x.py", # backslash
],
)
def test_rejects_unsafe_or_non_source_paths(path):
with pytest.raises(ValueError):
validate_package_files([[path, "abc123"]], package="x")
def test_rejects_malformed_hash():
with pytest.raises(ValueError):
validate_package_files([["ok.py", "../f"]], package="x")
def test_rejects_oversized_package():
entries = [[f"f{i}.py", "abc123"] for i in range(MAX_FILES_PER_PACKAGE + 1)]
with pytest.raises(ValueError):
validate_package_files(entries, package="x")
def test_rejects_malformed_entries():
with pytest.raises(ValueError):
validate_package_files([["only-one-element"]], package="x")
# ── filter_packages ─────────────────────────────────────────────────────────
INDEX = [
{"name": "ssd1306", "description": "OLED display driver"},
{"name": "umqtt.simple", "description": "MQTT client"},
{"name": "aiohttp", "description": "HTTP client/server with websockets"},
]
def test_name_prefix_ranks_before_description_hits():
got = [p["name"] for p in filter_packages(INDEX, "ss")]
assert got == ["ssd1306"]
got = [p["name"] for p in filter_packages(INDEX, "client")]
assert set(got) == {"umqtt.simple", "aiohttp"}
def test_empty_query_returns_everything():
assert filter_packages(INDEX, " ") == INDEX
def test_prefix_hits_come_first():
idx = [
{"name": "requests", "description": "umqtt helper"},
{"name": "umqtt.robust", "description": ""},
]
got = [p["name"] for p in filter_packages(idx, "umqtt")]
assert got == ["umqtt.robust", "requests"]