feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run
Some checks are pending
Qualité / tests (push) Waiting to run
This commit is contained in:
374
tests/test_app_store.py
Normal file
374
tests/test_app_store.py
Normal file
@@ -0,0 +1,374 @@
|
||||
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
|
||||
Reference in New Issue
Block a user