import json import pytest from furtka.manifest import Manifest, ManifestError, load_manifest VALID_MANIFEST = { "name": "fileshare", "display_name": "Network Files", "version": "0.1.0", "description": "SMB share", "volumes": ["files"], "ports": [445], "icon": "icon.svg", } def _write_app(tmp_path, name, payload): app_dir = tmp_path / name app_dir.mkdir() (app_dir / "manifest.json").write_text(json.dumps(payload)) return app_dir / "manifest.json" def test_load_valid_manifest(tmp_path): path = _write_app(tmp_path, "fileshare", VALID_MANIFEST) m = load_manifest(path) assert isinstance(m, Manifest) assert m.name == "fileshare" assert m.volumes == ("files",) assert m.ports == (445,) def test_volume_namespacing(tmp_path): path = _write_app(tmp_path, "fileshare", VALID_MANIFEST) m = load_manifest(path) assert m.volume_name("files") == "furtka_fileshare_files" def test_unknown_volume_raises(tmp_path): path = _write_app(tmp_path, "fileshare", VALID_MANIFEST) m = load_manifest(path) with pytest.raises(ManifestError): m.volume_name("does-not-exist") def test_missing_required_field(tmp_path): bad = dict(VALID_MANIFEST) del bad["display_name"] path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="display_name"): load_manifest(path) def test_name_must_match_when_expected_name_given(tmp_path): # Scanner passes expected_name= so /var/lib/furtka/apps/X/ # can't lie about its own identity. path = _write_app(tmp_path, "wrong-folder", VALID_MANIFEST) with pytest.raises(ManifestError, match="must equal 'wrong-folder'"): load_manifest(path, expected_name="wrong-folder") def test_name_check_skipped_without_expected_name(tmp_path): # Installer loads from arbitrary source paths (e.g. /tmp/my-tweaked-app/) # — the source folder name shouldn't matter, only the manifest's own name. path = _write_app(tmp_path, "any-folder-name", VALID_MANIFEST) m = load_manifest(path) assert m.name == "fileshare" def test_invalid_json(tmp_path): app = tmp_path / "fileshare" app.mkdir() (app / "manifest.json").write_text("{not json") with pytest.raises(ManifestError, match="invalid JSON"): load_manifest(app / "manifest.json") def test_volumes_wrong_type(tmp_path): bad = dict(VALID_MANIFEST, volumes="files") path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="volumes"): load_manifest(path) def test_ports_wrong_type(tmp_path): bad = dict(VALID_MANIFEST, ports=["445"]) path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="ports"): load_manifest(path) def test_settings_optional_default_empty(tmp_path): path = _write_app(tmp_path, "fileshare", VALID_MANIFEST) m = load_manifest(path) assert m.settings == () assert m.description_long == "" assert m.open_url == "" def test_open_url_stored_when_present(tmp_path): payload = dict(VALID_MANIFEST, open_url="smb://{host}/files") path = _write_app(tmp_path, "fileshare", payload) m = load_manifest(path) assert m.open_url == "smb://{host}/files" def test_open_url_non_string_rejected(tmp_path): payload = dict(VALID_MANIFEST, open_url=42) path = _write_app(tmp_path, "fileshare", payload) with pytest.raises(ManifestError, match="open_url"): load_manifest(path) def test_settings_parsed(tmp_path): payload = dict( VALID_MANIFEST, description_long="Long description with details.", settings=[ { "name": "SMB_USER", "label": "Benutzername", "description": "Anmeldename", "type": "text", "default": "furtka", "required": True, }, {"name": "SMB_PASSWORD", "label": "Passwort", "type": "password", "required": True}, ], ) path = _write_app(tmp_path, "fileshare", payload) m = load_manifest(path) assert m.description_long == "Long description with details." assert len(m.settings) == 2 assert m.settings[0].name == "SMB_USER" assert m.settings[0].label == "Benutzername" assert m.settings[0].default == "furtka" assert m.settings[0].type == "text" assert m.settings[0].required is True assert m.settings[1].type == "password" assert m.settings[1].default is None def test_settings_reject_lowercase_name(tmp_path): bad = dict(VALID_MANIFEST, settings=[{"name": "smb_user", "type": "text"}]) path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="UPPER_SNAKE_CASE"): load_manifest(path) def test_settings_reject_unknown_type(tmp_path): bad = dict(VALID_MANIFEST, settings=[{"name": "FOO", "type": "email"}]) path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="type must be one of"): load_manifest(path) def test_settings_reject_duplicate_name(tmp_path): bad = dict( VALID_MANIFEST, settings=[{"name": "FOO", "type": "text"}, {"name": "FOO", "type": "password"}], ) path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="duplicate"): load_manifest(path) def test_settings_non_list_rejected(tmp_path): bad = dict(VALID_MANIFEST, settings={"FOO": "bar"}) path = _write_app(tmp_path, "fileshare", bad) with pytest.raises(ManifestError, match="settings must be a list"): load_manifest(path)