From aa9c83b672d15b81f275694f1983654b8807b292 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Thu, 24 Sep 2026 08:25:03 -0500 Subject: [PATCH 1/2] Add authentication route wehre user can login logout and verify auth. --- tools/developer_tools/bely-cli/README.md | 20 ++++++++++-- tools/developer_tools/bely-cli/run_test.sh | 4 ++- .../bely-cli/src/bely_cli/auth.py | 5 +++ .../bely-cli/src/bely_cli/cli.py | 32 ++++++++++++++++--- .../bely-cli/src/bely_cli/commands.py | 24 +++++++++++++- 5 files changed, 76 insertions(+), 9 deletions(-) diff --git a/tools/developer_tools/bely-cli/README.md b/tools/developer_tools/bely-cli/README.md index 45e0af0a3..e7b9d3560 100644 --- a/tools/developer_tools/bely-cli/README.md +++ b/tools/developer_tools/bely-cli/README.md @@ -95,13 +95,19 @@ lookups (listing types, systems, templates, finding documents) do not. On success a token is cached at `~/.config/bely/token` (permissions `0600`) and reused on later runs. Expired or invalid tokens are discarded and you re-authenticate automatically. -To invalidate the current token on the server and remove it locally, run: +Authentication can also be managed explicitly: ```bash -bely-cli logout +bely-cli auth login +bely-cli auth verify +bely-cli auth logout ``` -If no token is cached, the command reports `Not logged in.` and succeeds. +`login` resolves and prompts for credentials using the rules above. `verify` checks the +cached token with the server, explains when no token is cached, and removes it if rejected. +`logout` invalidates the token on +the server and removes it locally; if no token is cached, it reports `Not logged in.` and +succeeds. The token location can be changed with the `token_path` setting; by default it sits beside the settings file (see [Configuration & environment](#configuration--environment)). @@ -124,6 +130,14 @@ bely-cli doc list --format json ## Commands +### `auth` — authentication + +- `bely-cli auth login` authenticates with configured or prompted credentials and caches the token. +- `bely-cli auth verify` checks whether the cached token is accepted by the server. +- `bely-cli auth logout` invalidates the current server session and removes the cached token. + +All three commands support the shared `--format` and `--no-prompt` options. + ### `doc` — log documents #### `bely-cli doc new` diff --git a/tools/developer_tools/bely-cli/run_test.sh b/tools/developer_tools/bely-cli/run_test.sh index 55daa3e80..3ca20275d 100755 --- a/tools/developer_tools/bely-cli/run_test.sh +++ b/tools/developer_tools/bely-cli/run_test.sh @@ -17,7 +17,9 @@ $RUNNER python -m unittest # (appended to a leaf command, not at the top level). $RUNNER bely-cli -h > /dev/null $RUNNER bely-cli doc list -h | grep -q -- --format -$RUNNER bely-cli logout -h | grep -q -- --format +$RUNNER bely-cli auth login -h | grep -q -- --format +$RUNNER bely-cli auth logout -h | grep -q -- --format +$RUNNER bely-cli auth verify -h | grep -q -- --format $RUNNER bely-cli tui lookup -h | grep -q -- --format # Bare `tui` is the one group that carries --limit/--format itself (see CLAUDE.md). $RUNNER bely-cli tui -h | grep -q -- --limit diff --git a/tools/developer_tools/bely-cli/src/bely_cli/auth.py b/tools/developer_tools/bely-cli/src/bely_cli/auth.py index c67e58fe0..d1a275f9d 100644 --- a/tools/developer_tools/bely-cli/src/bely_cli/auth.py +++ b/tools/developer_tools/bely-cli/src/bely_cli/auth.py @@ -119,6 +119,11 @@ def authenticated_factory_from_token(): return factory +def verify(): + """Return whether the cached token is valid, deleting it if rejected.""" + return authenticated_factory_from_token() is not None + + def logout(factory=None): """Invalidate and remove the cached token. Return False if no session exists.""" import belyApi diff --git a/tools/developer_tools/bely-cli/src/bely_cli/cli.py b/tools/developer_tools/bely-cli/src/bely_cli/cli.py index 7f9bd7b02..289059b90 100644 --- a/tools/developer_tools/bely-cli/src/bely_cli/cli.py +++ b/tools/developer_tools/bely-cli/src/bely_cli/cli.py @@ -7,7 +7,9 @@ from .commands import ( cmd_new_doc, cmd_list_docs, - cmd_logout, + cmd_auth_login, + cmd_auth_logout, + cmd_auth_verify, cmd_show_config, cmd_edit_config, cmd_set_config, @@ -57,11 +59,33 @@ def cli(): pass -@cli.command("logout") +# -- auth -- + +@cli.group("auth") +def auth_group(): + """Authentication commands.""" + pass + + +@auth_group.command("login") +@common_options +def auth_login(output_format): + """Log in and cache an authentication token.""" + cmd_auth_login(fmt=output_format) + + +@auth_group.command("logout") @common_options -def logout(output_format): +def auth_logout(output_format): """Log out and remove the cached authentication token.""" - cmd_logout(fmt=output_format) + cmd_auth_logout(fmt=output_format) + + +@auth_group.command("verify") +@common_options +def auth_verify(output_format): + """Verify the cached authentication token.""" + cmd_auth_verify(fmt=output_format) # -- doc -- diff --git a/tools/developer_tools/bely-cli/src/bely_cli/commands.py b/tools/developer_tools/bely-cli/src/bely_cli/commands.py index 2e9e761cc..b2da4bb2a 100644 --- a/tools/developer_tools/bely-cli/src/bely_cli/commands.py +++ b/tools/developer_tools/bely-cli/src/bely_cli/commands.py @@ -12,13 +12,35 @@ ENV_VARS = core.ENV_VARS -def cmd_logout(fmt="text"): +def cmd_auth_login(fmt="text"): + """Authenticate explicitly and cache the resulting token.""" + username = auth.get_username() + auth.login(username, auth.get_password(username)) + print_result({"authenticated": True, "username": username}, f"Logged in as {username}.", fmt) + + +def cmd_auth_logout(fmt="text"): """Invalidate the current token and remove it from the local cache.""" logged_out = auth.logout() message = "Logged out." if logged_out else "Not logged in." print_result({"logged_out": logged_out}, message, fmt) +def cmd_auth_verify(fmt="text"): + """Verify that the cached authentication token is valid.""" + if auth.load_token() is None: + print_result( + {"authenticated": False, "reason": "token_not_found"}, + "No cached authentication token found. Run 'bely-cli auth login' first.", + fmt, + ) + return + + authenticated = auth.verify() + message = "Authenticated." if authenticated else "Authentication token is invalid or expired." + print_result({"authenticated": authenticated}, message, fmt) + + def cmd_show_config(fmt="text"): """Show current configuration from settings file and environment.""" data = core.collect_config() From 07b56a9b6dd78e55f092e2a8e045a3ecc910ea1f Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Thu, 24 Sep 2026 08:25:15 -0500 Subject: [PATCH 2/2] Add tests for updated functionality. --- .../bely-cli/test/test_auth.py | 20 ++++++ .../bely-cli/test/test_commands.py | 64 ++++++++++++++++--- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/tools/developer_tools/bely-cli/test/test_auth.py b/tools/developer_tools/bely-cli/test/test_auth.py index 5cd7243c7..c2610e77b 100644 --- a/tools/developer_tools/bely-cli/test/test_auth.py +++ b/tools/developer_tools/bely-cli/test/test_auth.py @@ -123,6 +123,26 @@ def test_rejected_token_is_deleted_and_returns_none(self): self.assertIsNone(auth.load_token()) +class VerifyTests(AuthTestCase): + def test_valid_cached_token_returns_true(self): + self._install_factory(valid_token="good-token") + auth.save_token("good-token") + + self.assertTrue(auth.verify()) + + def test_missing_cached_token_returns_false(self): + self._install_factory(valid_token="good-token") + + self.assertFalse(auth.verify()) + + def test_rejected_cached_token_returns_false_and_deletes_token(self): + self._install_factory(valid_token="good-token") + auth.save_token("stale-token") + + self.assertFalse(auth.verify()) + self.assertIsNone(auth.load_token()) + + class LogoutTests(AuthTestCase): def test_no_cached_token_returns_false(self): self._install_factory(valid_token="good-token") diff --git a/tools/developer_tools/bely-cli/test/test_commands.py b/tools/developer_tools/bely-cli/test/test_commands.py index dabbe2c69..438078bb3 100644 --- a/tools/developer_tools/bely-cli/test/test_commands.py +++ b/tools/developer_tools/bely-cli/test/test_commands.py @@ -40,30 +40,76 @@ def add_update_log_entry(self, log_entry): return log_entry -class CmdLogoutTests(unittest.TestCase): - def test_logs_out_current_session(self): +class CmdAuthTests(unittest.TestCase): + def test_login_uses_resolved_credentials(self): + with patch.object(commands.auth, "get_username", return_value="alice"), \ + patch.object(commands.auth, "get_password", return_value="secret") as get_password, \ + patch.object(commands.auth, "login") as login: + buf = io.StringIO() + with redirect_stdout(buf): + commands.cmd_auth_login() + + get_password.assert_called_once_with("alice") + login.assert_called_once_with("alice", "secret") + self.assertEqual(buf.getvalue(), "Logged in as alice.\n") + + def test_logout_reports_current_session(self): with patch.object(commands.auth, "logout", return_value=True): buf = io.StringIO() with redirect_stdout(buf): - commands.cmd_logout() + commands.cmd_auth_logout() self.assertEqual(buf.getvalue(), "Logged out.\n") - def test_reports_when_not_logged_in(self): + def test_logout_reports_when_not_logged_in(self): with patch.object(commands.auth, "logout", return_value=False): buf = io.StringIO() with redirect_stdout(buf): - commands.cmd_logout() + commands.cmd_auth_logout() self.assertEqual(buf.getvalue(), "Not logged in.\n") - def test_structured_output_reports_status(self): - with patch.object(commands.auth, "logout", return_value=True): + def test_verify_reports_valid_token(self): + with patch.object(commands.auth, "load_token", return_value="token"), \ + patch.object(commands.auth, "verify", return_value=True): + buf = io.StringIO() + with redirect_stdout(buf): + commands.cmd_auth_verify(fmt="json") + + self.assertEqual(buf.getvalue(), '{"authenticated": true}\n') + + def test_verify_explains_when_token_is_not_found(self): + with patch.object(commands.auth, "load_token", return_value=None), \ + patch.object(commands.auth, "verify") as verify: + buf = io.StringIO() + with redirect_stdout(buf): + commands.cmd_auth_verify() + + verify.assert_not_called() + self.assertEqual( + buf.getvalue(), + "No cached authentication token found. Run 'bely-cli auth login' first.\n", + ) + + def test_verify_structured_output_identifies_missing_token(self): + with patch.object(commands.auth, "load_token", return_value=None): + buf = io.StringIO() + with redirect_stdout(buf): + commands.cmd_auth_verify(fmt="json") + + self.assertEqual( + buf.getvalue(), + '{"authenticated": false, "reason": "token_not_found"}\n', + ) + + def test_verify_reports_invalid_or_expired_token(self): + with patch.object(commands.auth, "load_token", return_value="token"), \ + patch.object(commands.auth, "verify", return_value=False): buf = io.StringIO() with redirect_stdout(buf): - commands.cmd_logout(fmt="json") + commands.cmd_auth_verify() - self.assertEqual(buf.getvalue(), '{"logged_out": true}\n') + self.assertEqual(buf.getvalue(), "Authentication token is invalid or expired.\n") class CmdNewDocTests(unittest.TestCase):