tests/test_project_library.py
text
from pathlib import Path
from zipfile import ZipFile
import pytest
from apps.whatsapp_control_api.app.api.main import (
_build_project_action_token,
_build_project_archive,
_encode_project_ref,
_inspect_project,
_project_is_protected,
_render_project_browser_page,
_render_project_card,
_render_delete_confirmation_page,
_scan_project_library,
_validate_project_action,
)
from apps.whatsapp_control_api.app.core.config import get_settings
@pytest.fixture(autouse=True)
def clear_settings_cache():
get_settings.cache_clear()
yield
get_settings.cache_clear()
def test_project_library_detects_web_project(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Coffee Website"
project.mkdir()
(project / "package.json").write_text('{"dependencies":{"next":"15.0.0"}}', encoding="utf-8")
(project / "page.tsx").write_text("export default function Page() {}", encoding="utf-8")
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
projects = _scan_project_library()
assert len(projects) == 1
assert projects[0]["name"] == "Coffee Website"
assert projects[0]["category"] == "web"
assert projects[0]["file_count"] == 2
assert "/projects/" in projects[0]["browse_url"]
def test_project_archive_excludes_secrets_and_dependencies(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Safe Project"
project.mkdir()
(project / "README.md").write_text("safe", encoding="utf-8")
(project / ".env").write_text("SECRET=unsafe", encoding="utf-8")
node_modules = project / "node_modules"
node_modules.mkdir()
(node_modules / "dependency.js").write_text("large dependency", encoding="utf-8")
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
archive = _build_project_archive(project)
with ZipFile(archive) as zip_file:
names = set(zip_file.namelist())
assert "Safe Project/README.md" in names
assert "Safe Project/.env" not in names
assert "Safe Project/node_modules/dependency.js" not in names
def test_project_action_token_validates_direct_child(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Download Me"
project.mkdir()
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
encoded_ref = _encode_project_ref(project)
token = _build_project_action_token(encoded_ref, "download")
validated = _validate_project_action(encoded_ref, token, "download")
assert validated == project.resolve()
def test_project_browse_token_validates_direct_child(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Browse Me"
project.mkdir()
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
encoded_ref = _encode_project_ref(project)
token = _build_project_action_token(encoded_ref, "browse")
validated = _validate_project_action(encoded_ref, token, "browse")
assert validated == project.resolve()
def test_inspect_project_detects_presentation(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Pitch Deck"
project.mkdir()
(project / "deck.pptx").write_bytes(b"presentation")
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
inspected = _inspect_project(project, tmp_path)
assert inspected["category"] == "presentation"
def test_system_project_is_protected(tmp_path: Path) -> None:
project = tmp_path / "AI Assistants"
project.mkdir()
assert _project_is_protected(project) is True
def test_delete_confirmation_requires_admin_token(tmp_path: Path) -> None:
project = tmp_path / "Delete Me"
project.mkdir()
html = _render_delete_confirmation_page(project, "encoded", "signed-token")
assert 'name="confirm_name"' in html
assert 'name="admin_token"' in html
assert 'name="confirm_delete"' in html
def test_project_card_links_to_file_browser(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Readable Project"
project.mkdir()
(project / "README.md").write_text("# Readable", encoding="utf-8")
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
html = _render_project_card(_inspect_project(project, tmp_path))
assert "Open files" in html
assert "Download ZIP" in html
def test_project_browser_renders_file_preview(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "Readable Project"
project.mkdir()
(project / "README.md").write_text("# Readable\n\nIsi project.", encoding="utf-8")
monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
get_settings.cache_clear()
encoded_ref = _encode_project_ref(project)
token = _build_project_action_token(encoded_ref, "browse")
html = _render_project_browser_page(project, encoded_ref, token, "README.md")
assert "Isi Project" in html
assert "README.md" in html
assert "Isi project." in html
assert "Download ZIP" in html