import json import pytest from furtka import installer from furtka.paths import apps_dir, bundled_apps_dir VALID_MANIFEST = { "name": "fileshare", "display_name": "Network Files", "version": "0.1.0", "description": "SMB share", "volumes": ["files"], "ports": [445], "icon": "icon.svg", } @pytest.fixture def fake_dirs(tmp_path, monkeypatch): apps = tmp_path / "apps" bundled = tmp_path / "bundled" apps.mkdir() bundled.mkdir() monkeypatch.setenv("FURTKA_APPS_DIR", str(apps)) monkeypatch.setenv("FURTKA_BUNDLED_APPS_DIR", str(bundled)) return apps, bundled def _write_app_source(root, name, manifest, env_example=None, env=None): app = root / name app.mkdir() (app / "manifest.json").write_text(json.dumps(manifest)) (app / "docker-compose.yaml").write_text("services: {}\n") if env_example is not None: (app / ".env.example").write_text(env_example) if env is not None: (app / ".env").write_text(env) return app def test_resolve_source_explicit_path(tmp_path, fake_dirs): src = _write_app_source(tmp_path, "fileshare", VALID_MANIFEST) resolved = installer.resolve_source(str(src)) assert resolved == src def test_resolve_source_bundled_name(fake_dirs): _, bundled = fake_dirs src = _write_app_source(bundled, "fileshare", VALID_MANIFEST) resolved = installer.resolve_source("fileshare") assert resolved == src def test_resolve_source_unknown_name(fake_dirs): with pytest.raises(installer.InstallError, match="not found"): installer.resolve_source("nope") def test_resolve_source_path_with_slash_must_exist(fake_dirs): with pytest.raises(installer.InstallError, match="not a directory"): installer.resolve_source("./does-not-exist") def test_install_from_copies_files(tmp_path, fake_dirs): src = _write_app_source(tmp_path, "fileshare", VALID_MANIFEST, env_example="A=1") target = installer.install_from(src) assert target == apps_dir() / "fileshare" assert (target / "manifest.json").exists() assert (target / "docker-compose.yaml").exists() assert (target / ".env.example").exists() # .env bootstrapped from .env.example since none was shipped assert (target / ".env").read_text() == "A=1" def test_install_from_preserves_existing_env(tmp_path, fake_dirs): src = _write_app_source(tmp_path, "fileshare", VALID_MANIFEST, env_example="A=new") target = apps_dir() / "fileshare" target.mkdir() (target / ".env").write_text("A=user-edited") installer.install_from(src) # User .env not clobbered. assert (target / ".env").read_text() == "A=user-edited" # But .env.example was updated. assert (target / ".env.example").read_text() == "A=new" def test_install_from_rejects_missing_manifest(tmp_path, fake_dirs): src = tmp_path / "broken" src.mkdir() with pytest.raises(installer.InstallError, match="manifest.json"): installer.install_from(src) def test_install_from_rejects_invalid_manifest(tmp_path, fake_dirs): bad = dict(VALID_MANIFEST) del bad["volumes"] src = _write_app_source(tmp_path, "fileshare", bad) with pytest.raises(installer.InstallError, match="volumes"): installer.install_from(src) def test_remove_deletes_folder(fake_dirs): apps, _ = fake_dirs (apps / "fileshare").mkdir() (apps / "fileshare" / "manifest.json").write_text("{}") installer.remove("fileshare") assert not (apps / "fileshare").exists() def test_remove_unknown_raises(fake_dirs): with pytest.raises(installer.InstallError, match="not installed"): installer.remove("ghost") def test_bundled_apps_dir_uses_env_override(fake_dirs): _, bundled = fake_dirs assert bundled_apps_dir() == bundled