Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
@@ -63,17 +63,35 @@ async def test_s3_presigned_urls_use_virtual_hosted_addressing() -> None:
@pytest.mark.asyncio
async def test_safety_contract_status_and_service_token() -> None:
task_id = str(uuid.uuid4())
async def handler(request: httpx.Request) -> httpx.Response:
assert request.headers["X-Service-Token"] == "safety-token"
assert request.url.path == "/internal/safety/v1/messages/check"
return httpx.Response(203, json={"verdict": "pending", "task_id": "task-1"})
assert request.url.path == "/internal/safety/v2/messages/check"
return httpx.Response(
202,
headers={
"Location": f"/internal/safety/v2/messages/tasks/{task_id}",
"Retry-After": "2",
},
json={
"verdict": "pending",
"processing_mode": "standard",
"config_version": 1,
"rules_version": "2026-01-01",
"task_id": task_id,
"poll_after_ms": 2000,
"expires_at": "2026-08-06T12:00:00Z",
},
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
result = await SafetyClient(settings(), http).check(
{"message_id": str(uuid.uuid4()), "content_kind": "text", "text": "hello"},
"request-1",
)
assert result == {"verdict": "pending", "task_id": "task-1", "_status": 203}
assert result["_status"] == 202
assert result["_location"] == f"/internal/safety/v2/messages/tasks/{task_id}"
@pytest.mark.asyncio
@@ -85,12 +103,23 @@ async def test_safety_file_body_uses_exact_attachment_schema() -> None:
assert body["attachment"] == {
"attachment_id": str(attachment_id),
"quarantine_object_key": "quarantine/users/u/file",
"quarantine_version_id": "version-1",
"quarantine_etag": '"etag-1"',
"mime_type": "application/pdf",
"size_bytes": 42,
"checksum": "sha256:" + "a" * 64,
}
assert "file" not in body
return httpx.Response(200, json={"verdict": "allow"})
return httpx.Response(
200,
json={
"verdict": "allow",
"processing_mode": "standard",
"config_version": 1,
"rules_version": "2026-01-01",
"rule_id": "safety.all_checks_passed",
},
)
payload = {
"message_id": str(uuid.uuid4()),
@@ -99,6 +128,8 @@ async def test_safety_file_body_uses_exact_attachment_schema() -> None:
"attachment": {
"attachment_id": str(attachment_id),
"quarantine_object_key": "quarantine/users/u/file",
"quarantine_version_id": "version-1",
"quarantine_etag": '"etag-1"',
"mime_type": "application/pdf",
"size_bytes": 42,
"checksum": "sha256:" + "a" * 64,
@@ -121,6 +152,47 @@ async def test_safety_auth_failure_is_dependency_failure() -> None:
)
@pytest.mark.asyncio
async def test_safety_poll_uses_location_and_rejects_untrusted_location() -> None:
task_id = str(uuid.uuid4())
async def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == f"/internal/safety/v2/messages/tasks/{task_id}"
return httpx.Response(
403,
json={
"verdict": "deny",
"processing_mode": "standard",
"config_version": 2,
"rules_version": "2026-08-06",
"rule_id": "file.malware_detected",
"reason_code": "message_blocked",
},
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
client = SafetyClient(settings(), http)
result = await client.poll(
f"/internal/safety/v2/messages/tasks/{task_id}", "request-1"
)
assert result["_status"] == 403
with pytest.raises(DependencyFailure, match="invalid_safety_location"):
await client.poll("https://attacker.example/task-1", "request-1")
@pytest.mark.asyncio
async def test_safety_fails_closed_on_malformed_success() -> None:
async def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"verdict": "allow"})
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
with pytest.raises(DependencyFailure, match="invalid_safety_response"):
await SafetyClient(settings(), http).check(
{"message_id": str(uuid.uuid4()), "content_kind": "text", "text": "hello"},
"request-1",
)
@pytest.mark.asyncio
async def test_openlines_contract_uses_bearer_and_idempotency() -> None:
message_id = uuid.uuid4()