Files
codexpi/tests/test_app_store.py
CodexPi 995b17a6e2
Some checks are pending
Qualité / tests (push) Waiting to run
feat: publish CodexPi contest snapshot
2026-11-14 20:23:59 +01:00

375 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from urllib.error import URLError
import pytest
from codexpi import app_store_service
from codexpi.app_store_service import AppStoreService
from codexpi.portfolio_catalog import CATALOG
def test_app_store_assigns_one_stable_endpoint_per_catalog_app(logged_in):
client, app, *_ = logged_in
response = client.get("/api/app-store/apps")
assert response.status_code == 200
apps = response.json()
assert len(apps) == 32
managed = [item for item in apps if item["managed"]]
deployed = [item for item in apps if not item["managed"]]
assert len(managed) == 18
assert len(deployed) == 14
assert len({item["endpoint"] for item in managed}) == 18
assert all(item["endpoint"] == f"/{item['slug']}/" for item in managed)
assert {item["slug"] for item in deployed} >= {"nasflix", "jellyfin", "youtube-carplay", "gameflix", "recette"}
assert next(item for item in apps if item["slug"] == "loutre-app-store")["launch_path"] == "/loutre-app-store/"
assert next(item for item in apps if item["slug"] == "pwa-factory")["open_url"] == "/pwa-factory/"
assert next(item for item in apps if item["slug"] == "pi-doctor")["open_url"] == "/pi-doctor/"
assert next(item for item in apps if item["slug"] == "codex-commander")["open_url"] == "/codex-commander/"
assert next(item for item in apps if item["slug"] == "autonomous-dev-studio")["open_url"] == "/autonomous-dev-studio/"
assert next(item for item in apps if item["slug"] == "loutre-finance")["open_url"] == "/loutre-finance/"
assert next(item for item in apps if item["slug"] == "smart-inventory")["open_url"] == "/smart-inventory/"
assert next(item for item in apps if item["slug"] == "auto-care")["open_url"] == "/auto-care/"
assert next(item for item in apps if item["slug"] == "print-pilot")["open_url"] == "/print-pilot/"
assert next(item for item in apps if item["slug"] == "loutre-home")["open_url"] == "/loutre-home/"
assert next(item for item in apps if item["slug"] == "network-guardian")["open_url"] == "/network-guardian/"
assert next(item for item in apps if item["slug"] == "reverse-proxy-manager")["open_url"] == "/reverse-proxy-manager/"
assert next(item for item in apps if item["slug"] == "loutre-brain")["open_url"] == "/loutre-brain/"
assert next(item for item in apps if item["slug"] == "knowledge-graph")["open_url"] == "/knowledge-graph/"
assert next(item for item in apps if item["slug"] == "trip-organizer")["open_url"] == "/trip-organizer/"
assert app.state.db.one("SELECT COUNT(*) AS n FROM app_store_apps")["n"] == 18
def test_app_store_is_a_codexpi_module():
project = next(item for item in CATALOG if item.slug == "loutre-app-store")
assert project.module_of == "CodexPi"
assert project.target_directory == "/home/config/CodexPi"
assert project.repository == "CodexPi"
def test_app_store_application_page_and_manifest_are_specific(logged_in):
client, *_ = logged_in
page = client.get("/loutre-os/", follow_redirects=False)
manifest = client.get("/loutre-os/manifest.webmanifest")
assert page.status_code == 200
assert 'rel="manifest" href="/loutre-os/manifest.webmanifest"' in page.text
assert "Interface de LoutreOS" in page.text
assert "Santé CPU, RAM, disque et température" in page.text
assert "Ouvrir" not in page.text
assert "data-pwa-tutorial hidden" in page.text
assert 'src="/loutre-app-store/pwa-status.js?v=1"' in page.text
assert manifest.status_code == 200
assert manifest.json()["start_url"] == "/loutre-os/"
assert manifest.json()["scope"] == "/loutre-os/"
assert manifest.json()["icons"][0]["src"] == "/loutre-app-store/app-icons/loutre-os-192.png"
def test_installed_pwa_stays_on_its_public_application_endpoint(logged_in):
client, app, *_ = logged_in
page = client.get("/pi-doctor/")
launched = client.get("/pi-doctor/?launch=1", follow_redirects=False)
manifest = client.get("/pi-doctor/manifest.webmanifest").json()
script = client.get("/pi-doctor/app.js")
assert "Centre de diagnostic" in page.text
assert 'src="/pi-doctor/app.js"' in page.text
assert "Activité de développement" not in page.text
assert ">Tâches<" not in page.text
assert launched.status_code == 200
assert "Centre de diagnostic" in launched.text
assert "État général" in launched.text
assert "Incidents" in launched.text
assert "Diagnostic Codex" in launched.text
assert "Réparation" in launched.text
assert "Historique" in launched.text
assert script.status_code == 200
assert script.headers["content-type"].startswith("application/javascript")
assert "'/api/pi-doctor'" in script.text
assert "textContent" in script.text
assert "innerHTML" not in script.text
assert manifest["start_url"] == "/pi-doctor/"
def test_autonomous_dev_studio_bookmark_opens_the_real_workflow(logged_in):
client, *_ = logged_in
page = client.get("/autonomous-dev-studio/")
script = client.get("/autonomous-dev-studio/app.js")
assert page.status_code == 200
assert "DU BESOIN À LA REVUE" in page.text
assert "Créer le plan à valider" in page.text
assert 'src="/autonomous-dev-studio/app.js"' in page.text
assert "Interface de Autonomous Dev Studio" not in page.text
assert ">Tâches<" not in page.text
assert script.status_code == 200
assert script.headers["content-type"].startswith("application/javascript")
assert "'/api/dev-studio/projects'" in script.text
assert "'/api/dev-studio/requests'" in script.text
assert "textContent" in script.text
assert "innerHTML" not in script.text
def test_codex_commander_bookmark_uses_real_codexpi_sessions(logged_in):
client, *_ = logged_in
page = client.get("/codex-commander/")
script = client.get("/codex-commander/app.js")
assert page.status_code == 200
assert "SESSIONS CODEX RÉELLES" in page.text
assert "Créer et démarrer la session Codex" in page.text
assert 'src="/codex-commander/app.js"' in page.text
assert "Interface de Codex Commander" not in page.text
assert script.status_code == 200
assert script.headers["content-type"].startswith("application/javascript")
assert "'/api/projects'" in script.text
assert "/conversations`" in script.text
assert "/queue`" in script.text
assert "/stop`" in script.text
assert "textContent" in script.text
assert "innerHTML" not in script.text
@pytest.mark.parametrize(
("slug", "destination"),
[
("nasflix-media-health", "https://loutrenas.myqnapcloud.com/#admin-integrity"),
("nasflix-ai-media-curator", "https://loutrenas.myqnapcloud.com/#curator"),
],
)
def test_nasflix_modules_open_their_real_existing_interfaces(logged_in, slug, destination):
client, *_ = logged_in
response = client.get(f"/{slug}/", follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"] == destination
def test_nasflix_modules_are_installable_only_after_real_route_is_registered(logged_in):
client, *_ = logged_in
apps = client.get("/api/app-store/apps").json()
assert next(item for item in apps if item["slug"] == "nasflix-media-health")["installable"] == 1
assert next(item for item in apps if item["slug"] == "nasflix-ai-media-curator")["installable"] == 1
def test_codex_commander_business_flow_creates_and_queues_real_session(logged_in, monkeypatch):
client, app, projects, _, headers = logged_in
folder = projects / "commander-real"
folder.mkdir()
project = client.post(
"/api/projects", headers=headers, json={"name": "Commander real", "path": str(folder)}
).json()
monkeypatch.setattr(app.state.codex, "detect", lambda: "/bin/true")
monkeypatch.setattr(app.state.codex, "_spawn", lambda *_args: 4242)
session = client.post(
f"/api/projects/{project['id']}/conversations",
headers=headers,
json={"title": "Agent Loutre · tester", "engine": "codex"},
)
assert session.status_code == 201
queued = client.post(
f"/api/conversations/{session.json()['id']}/queue",
headers=headers,
json={"content": "RÔLE COMMANDER : tester. Vérifie réellement les tests métier."},
)
assert queued.status_code == 202
persisted = client.get(f"/api/conversations/{session.json()['id']}/queue")
assert persisted.status_code == 200
assert persisted.json()[0]["content"].startswith("RÔLE COMMANDER : tester")
def test_app_store_share_button_uses_public_bookmark_endpoint(logged_in):
client, app, *_ = logged_in
app.state.app_store.launch_base_url = "https://codexpi.tail5a36dc.ts.net"
apps = client.get("/api/app-store/apps").json()
loutre_os = next(item for item in apps if item["slug"] == "loutre-os")
codexpi = next(item for item in apps if item["slug"] == "codexpi")
catalog = client.get("/loutre-app-store/")
expected = "https://codexpi.tail5a36dc.ts.net/loutre-os/"
assert loutre_os["launch_url"] == expected
assert codexpi["share_url"] == "https://codexpi.tail5a36dc.ts.net/"
assert 'data-url="/loutre-os/"' in catalog.text
assert 'aria-label="Partager LoutreOS pour l’ajouter à l’écran d’accueil"' in catalog.text
assert "PARTAGER" in catalog.text
assert ">OUVRIR<" not in catalog.text
assert client.get("/loutre-os/?launch=1", follow_redirects=False).status_code == 200
@pytest.mark.parametrize("value", [
"http://codexpi.example.test",
"https://user:password@codexpi.example.test",
"https://codexpi.example.test/untrusted/path",
"https://codexpi.example.test?next=https://evil.invalid",
])
def test_app_store_rejects_unsafe_launch_origins(app_env, value):
_, app, *_ = app_env
with pytest.raises(ValueError, match="origine HTTPS sûre"):
AppStoreService(app.state.db, value)
def test_app_store_serves_project_specific_icons(logged_in):
client, app, *_ = logged_in
icon_dir = app.state.settings.frontend_dist / "app-icons"
icon_dir.mkdir(parents=True)
(icon_dir / "loutre-os-192.png").write_bytes(b"project-icon")
(icon_dir / "loutre-os-apple.png").write_bytes(b"apple-icon")
icon = client.get("/loutre-os/icon-192.png")
apple = client.get("/loutre-os/apple-touch-icon.png")
assert icon.status_code == 200
assert icon.headers["content-type"] == "image/png"
assert icon.content == b"project-icon"
assert apple.status_code == 200
assert apple.content == b"apple-icon"
assert client.get("/loutre-os/icon-128.png").status_code == 404
stable = client.get("/loutre-app-store/app-icons/loutre-os-192.png")
assert stable.status_code == 200
assert stable.headers["content-type"] == "image/png"
assert stable.content == b"project-icon"
assert client.get("/loutre-app-store/app-icons/unknown-192.png").status_code == 404
def test_app_store_bookmark_requires_authentication(app_env):
client, app, *_ = app_env
icon_dir = app.state.settings.frontend_dist / "app-icons"
icon_dir.mkdir(parents=True)
(icon_dir / "loutre-app-store-apple.png").write_bytes(b"public-apple-icon")
assert client.get("/loutre-os/").status_code == 401
icon = client.get("/loutre-app-store/apple-touch-icon.png")
assert icon.status_code == 200
assert icon.headers["content-type"] == "image/png"
assert icon.content == b"public-apple-icon"
def test_loutre_app_store_renders_read_only_catalog(logged_in):
client, *_ = logged_in
response = client.get("/loutre-app-store/")
assert response.status_code == 200
assert response.headers["x-robots-tag"] == "noindex, nofollow"
assert "Loutre App Store" in response.text
assert 'data-url="/pi-doctor/"' in response.text
assert 'data-url="/loutre-os/"' in response.text
assert 'src="/loutre-app-store/app-icons/pi-doctor-192.png"' in response.text
assert 'src="/loutre-app-store/known-icons/nasflix.png"' in response.text
assert 'src="/loutre-app-store/share.js?v=2"' in response.text
assert 'class="featured"' in response.text
def test_app_store_does_not_label_unpublished_apps_as_local(logged_in):
client, *_ = logged_in
response = client.get("/loutre-app-store/")
assert response.status_code == 200
assert ">LOCAL<" not in response.text
assert "NON DÉPLOYÉE" in response.text
def test_app_store_share_script_guides_ios_to_safaris_own_share_button(logged_in):
client, *_ = logged_in
response = client.get("/loutre-app-store/share.js")
assert response.status_code == 200
assert response.headers["content-type"].startswith("application/javascript")
assert response.headers["x-content-type-options"] == "nosniff"
assert "navigator.share" not in response.text
assert "Ouvrir dans Safari" in response.text
assert "Sur l’écran d’accueil" in response.text
assert "window.open" in response.text
def test_application_page_hides_install_tutorial_in_standalone_mode(logged_in):
client, *_ = logged_in
response = client.get("/loutre-app-store/pwa-status.js")
assert response.status_code == 200
assert response.headers["content-type"].startswith("application/javascript")
assert "(display-mode: standalone)" in response.text
assert "navigator.standalone" in response.text
assert "tutorial.remove()" in response.text
def test_external_session_validation_fails_closed(monkeypatch):
class Response:
status = 200
def __enter__(self):
return self
def __exit__(self, *_args):
return None
class Opener:
def __init__(self, response=None, error=None):
self.response = response
self.error = error
def open(self, _request, timeout):
assert timeout == 3
if self.error:
raise self.error
return self.response
monkeypatch.setattr(app_store_service, "build_opener", lambda: Opener(Response()))
assert app_store_service.external_session_is_valid("http://nasflix/auth/me", "session=safe") is True
monkeypatch.setattr(app_store_service, "build_opener", lambda: Opener(error=URLError("offline")))
assert app_store_service.external_session_is_valid("http://nasflix/auth/me", "session=safe") is False
assert app_store_service.external_session_is_valid("http://nasflix/auth/me", "") is False
def test_app_store_rejects_external_or_colliding_launch_paths(logged_in):
client, *_rest, headers = logged_in
app = next(item for item in client.get("/api/app-store/apps").json() if item["slug"] == "loutre-os")
external = client.put(
f"/api/app-store/apps/{app['id']}",
headers=headers,
json={"launch_path": "https://example.invalid/steal"},
)
colliding = client.put(
f"/api/app-store/apps/{app['id']}",
headers=headers,
json={"launch_path": "/network-guardian/"},
)
assert external.status_code == 422
assert colliding.status_code == 422
def test_app_store_sync_preserves_admin_customization(logged_in):
client, *_rest, headers = logged_in
app = next(item for item in client.get("/api/app-store/apps").json() if item["slug"] == "loutre-os")
updated = client.put(
f"/api/app-store/apps/{app['id']}",
headers=headers,
json={"launch_path": "/?portfolio=custom-dashboard", "installable": False},
)
synced = client.post("/api/app-store/sync", headers=headers)
refreshed = next(item for item in client.get("/api/app-store/apps").json() if item["id"] == app["id"])
assert updated.status_code == 200
assert synced.status_code == 200
assert refreshed["launch_path"] == "/?portfolio=custom-dashboard"
assert refreshed["installable"] == 0