693cd952cd
仓库首次入库(此前仅存在于本地工作区与测试服构建产物中,未上传)。 改动(src/knowledge_adapter/asset_content_processor.py): - 新增 _TRUNCATED_TRAILING_MARKDOWN_RESOURCE_IMAGE 规则,清除 chunk 末尾被截断的  - 回归测试 tests/test_asset_content_processor.py:: test_truncated_trailing_markdown_resource_image_is_removed 验证证据: - 测试服 120.76.217.51,镜像 knowledge-adapter-service:reviewed-test-aeab4b5fca2d (release aeab4b5fca2d8661):KB d015d05c-25c8-4d63-b2ab-e8590d3c2dfb 5 个查询全部 http=200、records=5、残留 resource:// = 0;KB b9dacd62 无退化;容器日志无 error/exception - 本地:pytest 141 项全通过、ruff 全通过 - 该文件与 258c31e0fe07fd08 源码的差异经逐文件 md5 比对,为唯一变更文件
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
from uuid import UUID
|
|
|
|
import httpx
|
|
|
|
from knowledge_adapter.app import create_app
|
|
from knowledge_adapter.asset_redirect import (
|
|
AssetNotFoundError,
|
|
AssetRedirectUnavailableError,
|
|
)
|
|
|
|
ASSET_ID = UUID("c7f61f89-19ce-45b1-ac60-dbeed5e2a491")
|
|
SIGNED_URL = (
|
|
"https://private-assets.oss-cn-shenzhen.aliyuncs.com/"
|
|
"weknora/10008/assets/image.png?x-oss-signature=redacted"
|
|
)
|
|
|
|
|
|
class RecordingRedirectService:
|
|
def __init__(
|
|
self,
|
|
url: str = SIGNED_URL,
|
|
*,
|
|
error: Exception | None = None,
|
|
) -> None:
|
|
self.url = url
|
|
self.error = error
|
|
self.calls: list[UUID] = []
|
|
|
|
async def get_redirect_url(self, asset_id: UUID) -> str:
|
|
self.calls.append(asset_id)
|
|
if self.error is not None:
|
|
raise self.error
|
|
return self.url
|
|
|
|
|
|
async def test_asset_route_returns_anonymous_302() -> None:
|
|
service = RecordingRedirectService()
|
|
app = create_app(asset_redirect_service=service)
|
|
transport = httpx.ASGITransport(app=app)
|
|
|
|
async with httpx.AsyncClient(
|
|
transport=transport,
|
|
base_url="http://test",
|
|
follow_redirects=False,
|
|
) as client:
|
|
response = await client.get(f"/i/{ASSET_ID}")
|
|
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == SIGNED_URL
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert response.headers["referrer-policy"] == "no-referrer"
|
|
assert response.content == b""
|
|
assert service.calls == [ASSET_ID]
|
|
|
|
|
|
async def test_asset_route_returns_empty_404_for_unknown_asset() -> None:
|
|
service = RecordingRedirectService(error=AssetNotFoundError("asset not found"))
|
|
app = create_app(asset_redirect_service=service)
|
|
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(f"/i/{ASSET_ID}")
|
|
|
|
assert response.status_code == 404
|
|
assert response.content == b""
|
|
assert "asset" not in response.text.lower()
|
|
assert str(ASSET_ID) not in response.text
|
|
|
|
|
|
async def test_asset_route_rejects_malformed_uuid_before_service() -> None:
|
|
service = RecordingRedirectService()
|
|
app = create_app(asset_redirect_service=service)
|
|
transport = httpx.ASGITransport(app=app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/i/not-a-uuid")
|
|
|
|
assert response.status_code == 404
|
|
assert response.content == b""
|
|
assert service.calls == []
|
|
|
|
|
|
async def test_asset_route_returns_empty_503_when_redirect_is_unavailable() -> None:
|
|
service = RecordingRedirectService(
|
|
error=AssetRedirectUnavailableError(
|
|
"metadata http://100.100.100.200?token=secret object_key=private/image.png"
|
|
)
|
|
)
|
|
app = create_app(asset_redirect_service=service)
|
|
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(f"/i/{ASSET_ID}")
|
|
|
|
assert response.status_code == 503
|
|
assert response.content == b""
|
|
assert "100.100.100.200" not in response.text
|
|
assert "object_key" not in response.text
|
|
assert "secret" not in response.text
|
|
|
|
|
|
async def test_asset_route_does_not_accept_bucket_object_key_or_url_overrides() -> None:
|
|
service = RecordingRedirectService()
|
|
app = create_app(asset_redirect_service=service)
|
|
transport = httpx.ASGITransport(app=app)
|
|
|
|
async with httpx.AsyncClient(
|
|
transport=transport,
|
|
base_url="http://test",
|
|
follow_redirects=False,
|
|
) as client:
|
|
response = await client.get(
|
|
f"/i/{ASSET_ID}",
|
|
params={
|
|
"bucket": "attacker-bucket",
|
|
"object_key": "other/secret.png",
|
|
"url": "https://attacker.example/file",
|
|
},
|
|
headers={
|
|
"X-OSS-Bucket": "attacker-bucket",
|
|
"X-Object-Key": "other/secret.png",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
assert response.headers["location"] == SIGNED_URL
|
|
assert service.calls == [ASSET_ID]
|