Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions tools/developer_tools/bely-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand All @@ -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`
Expand Down
4 changes: 3 additions & 1 deletion tools/developer_tools/bely-cli/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions tools/developer_tools/bely-cli/src/bely_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 28 additions & 4 deletions tools/developer_tools/bely-cli/src/bely_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 --
Expand Down
24 changes: 23 additions & 1 deletion tools/developer_tools/bely-cli/src/bely_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
20 changes: 20 additions & 0 deletions tools/developer_tools/bely-cli/test/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
64 changes: 55 additions & 9 deletions tools/developer_tools/bely-cli/test/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading