初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
from __future__ import annotations
import hashlib
import io
import json
from pathlib import Path
import shutil
import tarfile
import zipfile
import pytest
from fastapi.testclient import TestClient
from app.main import create_app
from app.ota.diagnostics import clear_failure_log, persist_failure_log
from app.ota.package import OtaPackageError, build_package, extract_payload, inspect_package
from app.ota.state import write_last_result
from app.ota.versioning import read_feature_updated_at, SoftwareVersion, read_software_version
SOURCE_ROOT = Path(__file__).resolve().parents[1]
CURRENT_VERSION = read_software_version(SOURCE_ROOT)
FEATURE_UPDATED_AT = read_feature_updated_at(SOURCE_ROOT)
def following_version(version: SoftwareVersion) -> str:
return str(SoftwareVersion(version.major, version.minor, version.patch + 1))
def make_source(tmp_path: Path, version: str = "1.0.2") -> tuple[Path, Path]:
source = tmp_path / "source"
source.mkdir(parents=True)
(source / "VERSION").write_text(version + "\n", encoding="utf-8")
(source / "FEATURE_UPDATED_AT").write_text("2026-09-04T12:40+08:00\n", encoding="utf-8")
(source / "requirements.txt").write_text("fastapi==0.116.1\n", encoding="utf-8")
(source / "module.py").write_text("VALUE = 1\n", encoding="utf-8")
(source / "data").mkdir()
(source / "data" / "must-not-ship.json").write_text("{}", encoding="utf-8")
(source / "__pycache__").mkdir()
(source / "__pycache__" / "bad.pyc").write_bytes(b"bad")
wheelhouse = tmp_path / "wheelhouse"
wheelhouse.mkdir()
wheel = wheelhouse / "example.whl"
wheel.write_bytes(b"wheel")
digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
(wheelhouse / "SHA256SUMS").write_text(f"{digest} {wheel.name}\n", encoding="utf-8")
return source, wheelhouse
def make_package(tmp_path: Path, version: str = "1.0.2") -> Path:
source, wheelhouse = make_source(tmp_path, version)
package = tmp_path / f"matrix-screen-controller-{version}.ota"
build_package(
source,
wheelhouse,
package,
version=SoftwareVersion.parse(version),
release_notes="test release",
)
return package
def rewrite_zip(package: Path, replacements: dict[str, bytes]) -> None:
with zipfile.ZipFile(package, "r") as archive:
values = {name: archive.read(name) for name in archive.namelist()}
values.update(replacements)
package.unlink()
with zipfile.ZipFile(package, "w", compression=zipfile.ZIP_STORED) as archive:
for name, value in values.items():
archive.writestr(name, value)
def test_strict_software_version_ordering():
assert SoftwareVersion.parse("1.0.1") > SoftwareVersion.parse("1.0.0")
assert SoftwareVersion.parse("1.10.0") > SoftwareVersion.parse("1.2.99")
for invalid in ("v1.0.0", "1.0", "01.0.0", "1.0.0-beta"):
with pytest.raises(ValueError):
SoftwareVersion.parse(invalid)
def test_feature_updated_at_requires_valid_beijing_minute(tmp_path):
(tmp_path / "FEATURE_UPDATED_AT").write_text("2026-09-04T12:40+08:00\n", encoding="utf-8")
assert read_feature_updated_at(tmp_path) == "2026-09-04T12:40+08:00"
for invalid in (
"2026-09-04T12:40:00+08:00",
"2026-09-04T12:40Z",
"2026-09-04T04:40+00:00",
"2026-02-30T12:40+08:00",
):
(tmp_path / "FEATURE_UPDATED_AT").write_text(invalid + "\n", encoding="utf-8")
with pytest.raises(ValueError):
read_feature_updated_at(tmp_path)
(tmp_path / "FEATURE_UPDATED_AT").unlink()
with pytest.raises(RuntimeError, match="timestamp file is unreadable"):
read_feature_updated_at(tmp_path)
def test_full_package_build_inspect_extract_and_exclusions(tmp_path):
package = make_package(tmp_path)
info = inspect_package(package, current_version=SoftwareVersion.parse("1.0.0"))
assert str(info.target_version) == "1.0.2"
destination = tmp_path / "unpacked"
extract_payload(info, destination)
assert (destination / "software" / "module.py").is_file()
assert (destination / "software" / "FEATURE_UPDATED_AT").read_text(encoding="utf-8").strip() == "2026-09-04T12:40+08:00"
assert (destination / "wheelhouse" / "SHA256SUMS").is_file()
assert not (destination / "software" / "data").exists()
assert not (destination / "software" / "__pycache__").exists()
def test_package_carries_verified_frpc_system_dependency(tmp_path):
source, wheelhouse = make_source(tmp_path)
system_dependencies = tmp_path / "frpc"
system_dependencies.mkdir()
binary = system_dependencies / "frpc"
binary.write_bytes(b"official-aarch64-frpc")
digest = hashlib.sha256(binary.read_bytes()).hexdigest()
(system_dependencies / "SHA256SUMS").write_text(f"{digest} frpc\n", encoding="utf-8")
package = tmp_path / "with-frpc.ota"
build_package(
source,
wheelhouse,
package,
version=SoftwareVersion.parse("1.0.2"),
release_notes="FRP payload",
system_dependencies=system_dependencies,
)
destination = tmp_path / "with-frpc-unpacked"
extract_payload(inspect_package(package), destination)
assert (destination / "software" / "system-dependencies" / "frpc" / "frpc").read_bytes() == binary.read_bytes()
assert (destination / "software" / "system-dependencies" / "frpc" / "SHA256SUMS").is_file()
def test_same_or_older_version_is_rejected(tmp_path):
package = make_package(tmp_path)
for current in ("1.0.2", "1.0.3", "2.0.0"):
with pytest.raises(OtaPackageError, match="must be newer"):
inspect_package(package, current_version=SoftwareVersion.parse(current))
def test_payload_checksum_tampering_is_rejected(tmp_path):
package = make_package(tmp_path)
with zipfile.ZipFile(package, "r") as archive:
payload = archive.read("payload.tar.gz") + b"tampered"
rewrite_zip(package, {"payload.tar.gz": payload})
with pytest.raises(OtaPackageError, match="checksum or size"):
inspect_package(package)
def test_unsafe_tar_path_is_rejected_during_extract(tmp_path):
package = make_package(tmp_path)
with zipfile.ZipFile(package, "r") as archive:
manifest = json.loads(archive.read("manifest.json"))
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
content = b"escape"
member = tarfile.TarInfo("software/../../escape")
member.size = len(content)
tar.addfile(member, io.BytesIO(content))
payload = buffer.getvalue()
manifest["payload_sha256"] = hashlib.sha256(payload).hexdigest()
manifest["payload_bytes"] = len(payload)
manifest["expanded_bytes"] = len(b"escape")
rewrite_zip(package, {
"manifest.json": (json.dumps(manifest) + "\n").encode(),
"payload.tar.gz": payload,
})
info = inspect_package(package)
with pytest.raises(OtaPackageError, match="unsafe OTA payload path"):
extract_payload(info, tmp_path / "unsafe")
assert not (tmp_path / "escape").exists()
def test_ota_api_exposes_software_version_and_accepts_one_new_package(tmp_path):
project = tmp_path / "project"
project.mkdir()
target = following_version(CURRENT_VERSION)
package = make_package(tmp_path / "package", target)
app = create_app(
project_root=project,
driver_kind="mock",
ota_worker_starter=lambda: None,
ota_staging_root=tmp_path / "ota-staging",
)
with TestClient(app) as client:
status = client.get("/api/status").json()
assert status["service"]["software_version"] == str(CURRENT_VERSION)
assert status["service"]["feature_updated_at"] == FEATURE_UPDATED_AT
initial = client.get("/api/ota/status").json()
assert initial["current_version"] == str(CURRENT_VERSION)
assert initial["feature_updated_at"] == FEATURE_UPDATED_AT
assert initial["failure_log_available"] is False
assert initial["failure_log_bytes"] == 0
assert client.get("/api/ota/failure-log").status_code == 404
response = client.post(
f"/api/ota/update?filename=matrix-screen-controller-{target}.ota",
content=package.read_bytes(),
headers={"Content-Type": "application/octet-stream"},
)
assert response.status_code == 202
assert response.json()["job"]["target_version"] == target
assert client.get("/api/ota/status").json()["active"] is True
locked = client.put("/api/config", json={"brightness": 30})
assert locked.status_code == 423
concurrent = client.post(
f"/api/ota/update?filename=matrix-screen-controller-{target}.ota",
content=package.read_bytes(),
headers={"Content-Type": "application/octet-stream"},
)
assert concurrent.status_code == 409
def test_ota_api_rejects_same_version_before_starting_worker(tmp_path):
project = tmp_path / "project"
project.mkdir()
package = make_package(tmp_path / "package", str(CURRENT_VERSION))
starts = []
app = create_app(
project_root=project,
driver_kind="mock",
ota_worker_starter=lambda: starts.append(True),
ota_staging_root=tmp_path / "ota-staging",
)
with TestClient(app) as client:
response = client.post(
f"/api/ota/update?filename=matrix-screen-controller-{CURRENT_VERSION}.ota",
content=package.read_bytes(),
headers={"Content-Type": "application/octet-stream"},
)
assert response.status_code == 409
assert starts == []
def test_ota_failure_log_api_requires_failed_result_and_disables_caching(tmp_path):
project = tmp_path / "project"
project.mkdir()
app = create_app(project_root=project, driver_kind="mock")
data_root = app.state.ota_manager.data_root
runtime_log = tmp_path / "ota-worker.log"
runtime_log.write_text("<script>alert('safe')</script>\npytest failed\n", encoding="utf-8")
persist_failure_log(data_root, runtime_log)
with TestClient(app) as client:
assert client.get("/api/ota/failure-log").status_code == 404
write_last_result(data_root, {"status": "failed", "target_version": "9.9.9"})
status = client.get("/api/ota/status").json()
assert status["failure_log_available"] is True
assert status["failure_log_bytes"] == len(runtime_log.read_bytes())
response = client.get("/api/ota/failure-log")
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.headers["content-type"].startswith("text/plain")
assert response.content == runtime_log.read_bytes()
write_last_result(data_root, {"status": "success", "target_version": "9.9.9"})
assert client.get("/api/ota/status").json()["failure_log_available"] is False
assert client.get("/api/ota/failure-log").status_code == 404
clear_failure_log(data_root)