diff --git a/app/apps/impl/raspiblitz.py b/app/apps/impl/raspiblitz.py index b8cc08e..f72fb07 100644 --- a/app/apps/impl/raspiblitz.py +++ b/app/apps/impl/raspiblitz.py @@ -253,10 +253,14 @@ class RaspiBlitzApps(AppsBase): yield result async def uninstall_app(self, input: AppUninstallInput) -> AppManageResult: - async for result in self._manage_app(input.app_id, InstallMode.OFF): + async for result in self._manage_app( + input.app_id, InstallMode.OFF, keep_data=input.keep_data + ): yield result - async def _manage_app(self, app_id: AppId, mode: InstallMode) -> AppManageResult: + async def _manage_app( + self, app_id: AppId, mode: InstallMode, keep_data: bool = True + ) -> AppManageResult: """ Manages the installation or uninstallation process for a specified application. @@ -315,16 +319,21 @@ class RaspiBlitzApps(AppsBase): state=AppManagementProcessState.FAILURE, message=ErrorMessage( error_code=ApiErrors.APP_INVALID_FOR_PLATFORM, - detail=f"{app_id} not available for Core Lightning nodes", + detail=f"{app_id.value} not available for Core Lightning nodes", ), ) ) return + if installing: + params = "on" + else: + # the bonus scripts require an explicit keep/delete flag, + # otherwise they fall back to an interactive prompt + params = f"off {'--keep-data' if keep_data else '--delete-data'}" + try: - async for data in self.run_bonus_script( - app_id, "on" if installing else "off" - ): + async for data in self.run_bonus_script(app_id, params): if isinstance(data, str): yield Ok( AppManageTaskMessage( @@ -377,7 +386,7 @@ class RaspiBlitzApps(AppsBase): return try: - log_file_name = f"/var/cache/raspiblitz/temp/install.{app_id}.log" + log_file_name = f"/var/cache/raspiblitz/temp/install.{app_id.value}.log" stdout_full = "" stderr_full = "" data = {} diff --git a/conftest.py b/conftest.py index 4baa5a3..c4aa36c 100644 --- a/conftest.py +++ b/conftest.py @@ -20,6 +20,8 @@ _TEST_ENV_DEFAULTS = { "BAPI_BITCOIND_RPC_PW": "test", "BAPI_BITCOIND_ZMQ_BLOCK_RPC": "hashblock", "BAPI_BITCOIND_ZMQ_BLOCK_PORT": "28332", + # must be an existing directory for the raspiblitz apps impl to import + "BAPI_RB_SHELL_SCRIPT_PATH": "/tmp", } for _key, _value in _TEST_ENV_DEFAULTS.items(): diff --git a/tests/test_apps_uninstall_keep_data.py b/tests/test_apps_uninstall_keep_data.py new file mode 100644 index 0000000..d3e34db --- /dev/null +++ b/tests/test_apps_uninstall_keep_data.py @@ -0,0 +1,54 @@ +""" +Regression test: uninstalling an app must honour the keep_data choice. + +_manage_app dropped input.keep_data and always ran the bonus script with a +bare "off", so the RaspiBlitz bonus script fell back to an interactive +whiptail prompt (which hangs the non-interactive API) instead of the +requested keep/delete behaviour. +""" + +import pytest + +from app.apps.impl import raspiblitz +from app.apps.models import AppId, AppUninstallInput +from app.external.result_type.src.result.result import Ok + + +@pytest.fixture +def node(monkeypatch): + n = raspiblitz.RaspiBlitzApps() + + async def fake_valid(app_id, installing): + return Ok(None) + + monkeypatch.setattr(raspiblitz, "_validate_app_id", lambda app_id: Ok(None)) + monkeypatch.setattr(n, "_valid_installed_status", fake_valid) + return n + + +async def _run_uninstall(node, monkeypatch, keep_data: bool) -> str: + captured = {} + + async def fake_bonus(app_id, params): + captured["params"] = params + return + yield # unreachable; makes this an async generator + + monkeypatch.setattr(node, "run_bonus_script", fake_bonus) + + async for _ in node.uninstall_app( + AppUninstallInput(app_id=AppId.MEMPOOL, keep_data=keep_data) + ): + pass + + return captured["params"] + + +async def test_uninstall_delete_data(node, monkeypatch): + params = await _run_uninstall(node, monkeypatch, keep_data=False) + assert params == "off --delete-data" + + +async def test_uninstall_keep_data(node, monkeypatch): + params = await _run_uninstall(node, monkeypatch, keep_data=True) + assert params == "off --keep-data"