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).
101 lines
3.2 KiB
Python
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"]
|