13a2867ef6
End-to-end implementation per docs/sprint-2.5-plan.md. New requirement added by user mid-sprint: 4-role RBAC (Super Admin / Engineer / Owner / User) with dual-auth for Engineer flashing firmware, plus a "mini Arduino IDE" inside the Studio. Tests: pytest 231/231 green (129 Sprint 2 + 102 Sprint 2.5 new). RBAC core (arautopilot/core/): - rbac.py: 4 roles, 12 capabilities, immutable capability matrix, has() / capabilities_of() / require() / requires_dual_auth() helpers. Engineer flashing firmware needs SA approval; everything else is single-factor. - user.py: User model with PBKDF2-HMAC-SHA256 PIN hashing (200k iters, 16-byte salt, self-describing hash format for future migrations). 4-8 digit numeric PINs enforced. - user_store.py: JSON-backed user database. seed_demo_users() for first-run UX. - audit.py: append-only JSONL audit log. AuditEvent with timestamp, user_id, role, action, target, outcome, reason, secondary_user_id for dual-auth, optional extra payload. Crypto signing of lines deferred to Sprint 8. Studio GUI (arautopilot/studio/): - app.py: real entry point (replaces Sprint 0 stub). --seed-demo populates demo users without launching GUI; --data-dir overrides the ~/.ar-autopilot/studio/ default. - session.py: Session + SessionHolder. check() always audits the decision; verify_super_admin_pin() + log_dual_auth_grant() for dual-auth flows. - login_window.py: modal login dialog with user picker + PIN field. Audits login attempts (success and bad-PIN denials). - main_window.py: top-level window with sidebar (user + role + caps) and tab area (Overview, Flash Console, Project placeholder, Telemetry placeholder). - flash_console.py: the "mini Arduino IDE". Lists serial ports via pyserial; picks firmware variant (esp32-dev / esp32-debug); compiles via 'pio run'; flashes via 'pio run -t upload --upload-port <port>'; streams pio output to a dark-themed read-only console; supports cancel. For Engineer flashes, asks the Super Admin for their PIN inline before invoking pio. Records dual-auth grant + pio exit code in the audit log. Dependencies: - New [project.optional-dependencies] group 'studio': PySide6>=6.6, pyserial>=3.5, platformio>=6.1. Kept optional so the core can be installed in lean / CI environments. Tests (arautopilot/tests/): - test_rbac.py: 32 tests for capability matrix, dual-auth policy, no-privilege-escalation invariants, partial overlap between roles. - test_user.py: 11 tests for PIN hashing, verification, salting, serialisation, field validators. - test_audit.py: 9 tests for JSONL append, immutability, round-trip, corrupt-line detection, dual-auth event shape, blank-line tolerance. - test_user_store.py: 10 tests for CRUD, persistence, role filtering, demo seed idempotency. - test_session.py: 9 tests for capability checks + audit side effects, SA PIN verification, dual-auth recording, SessionHolder lifecycle. - test_studio_smoke.py: 5 headless tests verifying Studio modules import without a display server, --seed-demo works, helpers safe to call without hardware. NOT in Sprint 2.5 (intentional): - Crypto signing of audit log lines (hash-chain) -- Sprint 8 - HWID binding of the user store -- Sprint 8 - Project configurator + .appack compiler -- Sprint 4 - Flutter bridge display -- Sprint 4 - Telemetry dashboard tab -- Sprint 4 - Serial monitor as a separate tab -- future enhancement Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""Smoke tests for the Studio entry point.
|
|
|
|
These tests verify that:
|
|
|
|
- The Studio modules import cleanly without a display server (no Qt
|
|
classes are instantiated at module level).
|
|
- ``arautopilot.studio.app.run(['--seed-demo', '--data-dir', tmp])``
|
|
populates a fresh user store and exits 0 without launching a GUI.
|
|
|
|
GUI behaviour (login modal, main window, Flash Console widget rendering)
|
|
is covered by manual smoke testing -- pytest is intentionally headless
|
|
here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
def test_studio_modules_import_cleanly() -> None:
|
|
# Importing these must not require a display server.
|
|
from arautopilot.studio import app # noqa: F401
|
|
from arautopilot.studio import flash_console # noqa: F401
|
|
from arautopilot.studio import login_window # noqa: F401
|
|
from arautopilot.studio import main_window # noqa: F401
|
|
from arautopilot.studio import session # noqa: F401
|
|
|
|
|
|
def test_seed_demo_via_app_run(tmp_path: Path) -> None:
|
|
from arautopilot.core.user_store import UserStore
|
|
from arautopilot.studio.app import run
|
|
|
|
rc = run(["--seed-demo", "--data-dir", str(tmp_path)])
|
|
assert rc == 0
|
|
store = UserStore(tmp_path / "users.json")
|
|
assert len(store) == 4
|
|
sa = store.find_by_name("Alvaro")
|
|
assert sa is not None
|
|
assert sa.verify_pin("1111")
|
|
|
|
|
|
def test_seed_demo_is_idempotent(tmp_path: Path) -> None:
|
|
from arautopilot.core.user_store import UserStore
|
|
from arautopilot.studio.app import run
|
|
|
|
assert run(["--seed-demo", "--data-dir", str(tmp_path)]) == 0
|
|
assert run(["--seed-demo", "--data-dir", str(tmp_path)]) == 0
|
|
store = UserStore(tmp_path / "users.json")
|
|
assert len(store) == 4
|
|
|
|
|
|
def test_flash_console_helper_finds_pio_or_returns_path() -> None:
|
|
"""`_find_pio` must return a Path object even if the venv path doesn't
|
|
exist (falls back to the bare 'pio' name)."""
|
|
from arautopilot.studio.flash_console import _find_pio
|
|
p = _find_pio()
|
|
assert p is not None
|
|
# We don't assert existence -- the function falls back to the bare name.
|
|
from pathlib import Path as _P
|
|
assert isinstance(p, _P)
|
|
|
|
|
|
def test_list_serial_ports_is_safe_to_call() -> None:
|
|
"""It must work whether or not pyserial finds ports."""
|
|
from arautopilot.studio.flash_console import list_serial_ports
|
|
result = list_serial_ports()
|
|
assert isinstance(result, list)
|
|
for entry in result:
|
|
assert isinstance(entry, tuple)
|
|
assert len(entry) == 2
|