diff --git a/README.md b/README.md index 7a1e49c..6141957 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ fireblocks-cli --help | `init` | ✅ | ✅ | Initialize the default configuration files | Creates `~/.config/fireblocks-cli/config.toml` and `~/.config/fireblocks-cli/keys/` | | `gen-keys` | ✅ | ✅ | Generate Fireblocks-compatible private key and CSR | Outputs to `.config/fireblocks-cli/keys/{name}.csr`, etc. | | `list` | n/a | n/a | List all configured profiles | Displays `[profile]` sections from `config.toml` | -| `edit` | n/a | n/a | Open the config file in your default `$EDITOR` | Falls back to `vi` or `nano` if `$EDITOR` is not set | +| `edit` | ✅ | ✅ | Open the config file in your default `$EDITOR` | Falls back to `vi` or `nano` if `$EDITOR` is not set | | `validate` | ✅ | n/a | Validate the structure and contents of the config file | Checks for invalid or missing keys and values | | `add` | n/a | n/a | Append a new profile to the configuration file | Will add to the bottom of the file without auto-formatting | | `remove` | n/a | n/a | Remove a profile from the configuration | Deletes the corresponding section from `config.toml` | diff --git a/fireblocks_cli/commands/configure.py b/fireblocks_cli/commands/configure.py index 216b779..d88bb07 100644 --- a/fireblocks_cli/commands/configure.py +++ b/fireblocks_cli/commands/configure.py @@ -139,3 +139,50 @@ def validate_file(path: Path): "This may cause unexpected behavior.", fg=typer.colors.YELLOW, ) + + +@configure_app.command("edit") +def edit(): + """ + Open the config.toml file in your default editor ($EDITOR). + """ + import os + import subprocess + from fireblocks_cli.config import get_config_file + + config_path = get_config_file() + + if not config_path.exists(): + typer.secho(f"❌ Config file not found: {config_path}", fg=typer.colors.RED) + raise typer.Exit(code=1) + + editor = os.environ.get("EDITOR") + + if not editor: + # Fallbacks + for fallback in ["code", "nano", "vi"]: + if shutil.which(fallback): + editor = fallback + break + + if not editor: + typer.secho( + "❌ No editor found. Please set the $EDITOR environment variable.", + fg=typer.colors.RED, + ) + raise typer.Exit(code=1) + + try: + subprocess.run([editor, str(config_path)]) + except Exception as e: + typer.secho(f"❌ Failed to open editor: {e}", fg=typer.colors.RED) + raise typer.Exit(code=1) + # Validate after editing + typer.echo("\n🔍 Validating config.toml after editing...\n") + try: + from fireblocks_cli.commands.configure import validate + + validate() + except Exception as e: + typer.secho(f"❌ Validation failed: {e}", fg=typer.colors.RED) + raise typer.Exit(code=1) diff --git a/tests/test_configure_edit.py b/tests/test_configure_edit.py new file mode 100644 index 0000000..6d4d9ad --- /dev/null +++ b/tests/test_configure_edit.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 +# Author: Shohei KAMON + +import os +import pytest +from typer.testing import CliRunner +from fireblocks_cli.main import app +from fireblocks_cli.config import get_config_file +from pathlib import Path + +runner = CliRunner() + + +@pytest.fixture +def mock_home(tmp_path, monkeypatch): + """ + Redirect HOME to a temporary directory to isolate test artifacts. + + Creates: + - ~/.config/fireblocks-cli/config.toml with dummy valid content + """ + monkeypatch.setattr(Path, "home", lambda: tmp_path) + config_path = tmp_path / ".config" / "fireblocks-cli" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text( + '[default]\napi_id = "test"\napi_secret_key = { type = "file", value = "xxx" }\n' + ) + return tmp_path + + +def test_edit_invokes_editor_and_validates(mock_home, monkeypatch): + """ + Test that `configure edit`: + - Launches the configured editor via subprocess.run + - Validates config.toml after editing + """ + + # Flags to confirm mock functions are called + called = {"editor": False, "validate": False} + + # Mock subprocess.run (simulate editor launch) + def mock_run(cmd, *args, **kwargs): + assert get_config_file().name in cmd[-1] + called["editor"] = True + + # Mock validate() call + def mock_validate(): + called["validate"] = True + + monkeypatch.setenv("EDITOR", "dummy-editor") + monkeypatch.setattr("subprocess.run", mock_run) + monkeypatch.setattr("fireblocks_cli.commands.configure.validate", mock_validate) + + result = runner.invoke(app, ["configure", "edit"]) + assert result.exit_code == 0 + assert called["editor"] is True + assert called["validate"] is True