mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Closes the last gap in the one-way Hanzo control surface. IAM/KMS/PaaS already had both CLI subcommands and MCP tools; S3 had neither. - hanzo s3: buckets/mb/rb/ls/stat/rm/presign over hanzo_s3 (MinIO) client, creds from env (inject from KMS, never plaintext). Registered in the unified hanzo CLI alongside iam/kms/paas. - hanzo-tools-s3: S3Tool MCP tool (same action-dispatch pattern as PaaSTool), exported via the hanzo.tools entry point; s3 added to loader prefix map. - uv workspace: map hanzo-cli/iam/kms/s3 + tools-s3 to local sources so the unified CLI resolves on-disk packages (fixes hanzo_iam ImportError from a stale published 1.29.0 wheel shadowing local 1.30.0). Tests: 7 CLI + 14 MCP, all green.
78 lines
2.3 KiB
Python
78 lines
2.3 KiB
Python
"""Unit tests for the `hanzo s3` CLI group (mocked S3 client)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from hanzo_cli.s3.commands import s3
|
|
|
|
|
|
def _patch_client(client):
|
|
return patch("hanzo_cli.s3.commands._get_s3_client", return_value=client)
|
|
|
|
|
|
def test_s3_group_help():
|
|
result = CliRunner().invoke(s3, ["--help"])
|
|
assert result.exit_code == 0
|
|
for cmd in ("buckets", "mb", "rb", "ls", "stat", "rm", "presign"):
|
|
assert cmd in result.output
|
|
|
|
|
|
def test_buckets_lists():
|
|
client = MagicMock()
|
|
b = MagicMock(creation_date=datetime(2026, 1, 1))
|
|
b.name = "hanzo-space"
|
|
client.list_buckets.return_value = [b]
|
|
with _patch_client(client):
|
|
result = CliRunner().invoke(s3, ["buckets"])
|
|
assert result.exit_code == 0
|
|
assert "hanzo-space" in result.output
|
|
|
|
|
|
def test_make_bucket():
|
|
client = MagicMock()
|
|
with _patch_client(client):
|
|
result = CliRunner().invoke(s3, ["mb", "b1"])
|
|
assert result.exit_code == 0
|
|
client.make_bucket.assert_called_once_with("b1")
|
|
|
|
|
|
def test_ls_objects():
|
|
client = MagicMock()
|
|
obj = MagicMock(object_name="a.txt", size=12, is_dir=False,
|
|
last_modified=datetime(2026, 1, 1))
|
|
client.list_objects.return_value = [obj]
|
|
with _patch_client(client):
|
|
result = CliRunner().invoke(s3, ["ls", "b1", "--prefix", "a"])
|
|
assert result.exit_code == 0
|
|
assert "a.txt" in result.output
|
|
|
|
|
|
def test_presign_outputs_url():
|
|
client = MagicMock()
|
|
client.presigned_get_object.return_value = "https://s3.hanzo.ai/b1/a.txt?sig=x"
|
|
with _patch_client(client):
|
|
result = CliRunner().invoke(s3, ["presign", "b1", "a.txt"])
|
|
assert result.exit_code == 0
|
|
assert "https://s3.hanzo.ai/" in result.output
|
|
|
|
|
|
def test_rm_confirms(monkeypatch):
|
|
client = MagicMock()
|
|
with _patch_client(client):
|
|
# Decline confirmation → aborts, client not called.
|
|
result = CliRunner().invoke(s3, ["rm", "b1", "a.txt"], input="n\n")
|
|
assert result.exit_code != 0
|
|
client.remove_object.assert_not_called()
|
|
|
|
|
|
def test_missing_credentials():
|
|
runner = CliRunner()
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
result = runner.invoke(s3, ["buckets"])
|
|
assert result.exit_code != 0
|
|
assert "credentials" in result.output.lower()
|