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_arbitrary_source_folder_name(tmp_path, fake_dirs): # Source folder named "downloaded-fileshare-fork-v2" but manifest says # "fileshare" — install lands at /var/lib/furtka/apps/fileshare/ regardless. src = _write_app_source( tmp_path, "downloaded-fileshare-fork-v2", VALID_MANIFEST, env_example="A=real-value", ) target = installer.install_from(src) assert target.name == "fileshare" assert (target / "manifest.json").exists() 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 def test_install_refuses_placeholder_password(tmp_path, fake_dirs): src = _write_app_source( tmp_path, "fileshare", VALID_MANIFEST, env_example="SMB_PASSWORD=changeme" ) with pytest.raises(installer.InstallError, match="placeholder values for SMB_PASSWORD"): installer.install_from(src) # Files should still have landed so the user can vim the .env in place. target = apps_dir() / "fileshare" assert (target / ".env").exists() assert (target / "manifest.json").exists() def test_install_succeeds_after_user_edits_env(tmp_path, fake_dirs): # First run: refuses placeholder. src = _write_app_source( tmp_path, "fileshare", VALID_MANIFEST, env_example="SMB_PASSWORD=changeme" ) with pytest.raises(installer.InstallError): installer.install_from(src) # User edits the live .env to a real secret. target = apps_dir() / "fileshare" (target / ".env").write_text("SMB_PASSWORD=hunter2\n") # Re-run: now succeeds, user .env preserved. installer.install_from(src) assert (target / ".env").read_text().strip() == "SMB_PASSWORD=hunter2" def test_install_locks_env_permissions(tmp_path, fake_dirs): src = _write_app_source( tmp_path, "fileshare", VALID_MANIFEST, env_example="SMB_PASSWORD=hunter2" ) installer.install_from(src) target = apps_dir() / "fileshare" mode = (target / ".env").stat().st_mode & 0o777 assert mode == 0o600, f"expected 0o600 on .env, got {oct(mode)}" def test_placeholder_check_ignores_comments_and_blanks(tmp_path, fake_dirs): src = _write_app_source( tmp_path, "fileshare", VALID_MANIFEST, env_example="# default values\n\nSMB_PASSWORD=real-secret\n", ) # Should NOT raise — only commented "changeme" mentions, no actual placeholder. installer.install_from(src) def test_placeholder_check_handles_quoted_values(tmp_path, fake_dirs): src = _write_app_source( tmp_path, "fileshare", VALID_MANIFEST, env_example='SMB_PASSWORD="changeme"\n', ) with pytest.raises(installer.InstallError, match="placeholder"): installer.install_from(src)