DocHub/backend/tests/test_garage_client.py
Tianyang 360e9e5a14 refactor: rename all minio references to garage, drop minio SDK for boto3
The backend no longer mentions MinIO anywhere: MinioClient -> GarageClient
(minio_client.py -> garage_client.py), MINIO_* env vars -> GARAGE_*, and
the `minio` Python package is replaced with `boto3` (AWS's generic S3
client) since Garage is the only storage backend this project targets
now. S3 API semantics are unchanged, so document_service.py's behavior
is identical — this is a naming and dependency swap, not a logic change.

Also updates test fixtures/names (FakeMinioClient -> FakeGarageClient,
FakeMinioSDK -> FakeS3SDK) and storage/README.md, which no longer frames
Garage as a MinIO replacement (that migration note has served its purpose
now that no MinIO reference remains in the codebase).

Backend: 54/54 tests passing. Frontend: 38/38 tests passing (unaffected,
verified for regressions since it talks to the backend only through the
unchanged REST API).
2026-08-01 10:51:49 +08:00

101 lines
3.2 KiB
Python

from datetime import datetime, timezone
import pytest
from botocore.exceptions import ClientError
from app.garage_client import GarageClient
class _FakePaginator:
def __init__(self, sdk):
self._sdk = sdk
def paginate(self, Bucket, Prefix="", Delimiter="/"):
contents = []
for key, (data, content_type, last_modified) in self._sdk.objects.items():
if key.startswith(Prefix):
contents.append({"Key": key, "Size": len(data), "LastModified": last_modified})
yield {"Contents": contents, "CommonPrefixes": []}
class FakeS3SDK:
def __init__(self):
self.objects = {}
self.put_calls = []
self.removed_keys = []
def get_paginator(self, name):
return _FakePaginator(self)
def get_object(self, Bucket, Key):
if Key not in self.objects:
raise ClientError({"Error": {"Code": "NoSuchKey"}}, "GetObject")
data, _, _ = self.objects[Key]
class _Body:
def read(self_inner):
return data
return {"Body": _Body()}
def put_object(self, Bucket, Key, Body, ContentLength, ContentType):
data = Body.read()
self.objects[Key] = (data, ContentType, datetime.now(timezone.utc))
self.put_calls.append((Bucket, Key, ContentType))
def delete_object(self, Bucket, Key):
del self.objects[Key]
self.removed_keys.append(Key)
def test_list_objects_returns_keys_matching_prefix():
sdk = FakeS3SDK()
sdk.objects["产品文档/架构设计.md"] = (b"# hi", "text/markdown", datetime(2026, 8, 1, tzinfo=timezone.utc))
sdk.objects["团队规范/编码规范.md"] = (b"# rules", "text/markdown", datetime(2026, 8, 1, tzinfo=timezone.utc))
client = GarageClient(sdk, bucket="dochub")
result = client.list_objects("产品文档/")
assert len(result) == 1
assert result[0]["key"] == "产品文档/架构设计.md"
assert result[0]["size"] == 4
def test_get_object_returns_bytes():
sdk = FakeS3SDK()
sdk.objects["产品文档/架构设计.md"] = (b"# architecture", "text/markdown", datetime.now(timezone.utc))
client = GarageClient(sdk, bucket="dochub")
data = client.get_object("产品文档/架构设计.md")
assert data == b"# architecture"
def test_get_object_raises_file_not_found_for_missing_key():
sdk = FakeS3SDK()
client = GarageClient(sdk, bucket="dochub")
with pytest.raises(FileNotFoundError):
client.get_object("不存在/文档.md")
def test_put_object_stores_data_with_content_type():
sdk = FakeS3SDK()
client = GarageClient(sdk, bucket="dochub")
client.put_object("产品文档/新文档.md", b"# new doc", "text/markdown")
assert sdk.objects["产品文档/新文档.md"][0] == b"# new doc"
assert sdk.put_calls == [("dochub", "产品文档/新文档.md", "text/markdown")]
def test_delete_object_removes_key():
sdk = FakeS3SDK()
sdk.objects["产品文档/旧文档.md"] = (b"old", "text/markdown", datetime.now(timezone.utc))
client = GarageClient(sdk, bucket="dochub")
client.delete_object("产品文档/旧文档.md")
assert "产品文档/旧文档.md" not in sdk.objects
assert sdk.removed_keys == ["产品文档/旧文档.md"]