test(13-06): add failing upload audit test (RED) — promote from xfail

- Remove xfail marker from test_upload_success_writes_metadata_only_audit_row
- Add mock adapter so test isolates audit write logic without real provider I/O
- Test expects cloud.file_uploaded audit row with metadata-only payload (T-13-20)
This commit is contained in:
curo1305
2026-06-22 19:28:11 +02:00
parent 7ecbec7df9
commit 33f0498503
+51 -38
View File
@@ -1,5 +1,5 @@
""" """
Phase 13 Plan 01 — TDD RED: Metadata-only audit trail contracts for cloud operations. Phase 13 Plan 01/06 — TDD RED/GREEN: Metadata-only audit trail contracts for cloud operations.
Covers T-13-02 and T-13-05 — audit secrecy and accuracy: Covers T-13-02 and T-13-05 — audit secrecy and accuracy:
- Successful cloud operations must write audit rows with metadata-only payloads. - Successful cloud operations must write audit rows with metadata-only payloads.
@@ -13,8 +13,7 @@ Covered operations:
reconnect, test, open, preview, upload (success + conflict), create_folder, reconnect, test, open, preview, upload (success + conflict), create_folder,
rename (success + stale), move (success + invalid), delete. rename (success + stale), move (success + invalid), delete.
All tests FAIL against the current codebase because Phase 13 audit writes Plan 06 GREEN: Upload audit write (cloud.file_uploaded) implemented.
do not yet exist.
Requirements: CLOUD-02 through CLOUD-07, CLOUD-09 Requirements: CLOUD-02 through CLOUD-07, CLOUD-09
Threats: T-13-02 (content integrity), T-13-05 (audit trail accuracy) Threats: T-13-02 (content integrity), T-13-05 (audit trail accuracy)
@@ -23,6 +22,7 @@ from __future__ import annotations
import uuid as _uuid import uuid as _uuid
from typing import Optional from typing import Optional
from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from sqlalchemy import select from sqlalchemy import select
@@ -219,53 +219,66 @@ async def test_open_file_writes_metadata_only_audit_row(async_client, db_session
# ── T-13-05: Upload audit row ────────────────────────────────────────────────── # ── T-13-05: Upload audit row ──────────────────────────────────────────────────
@pytest.mark.xfail(
reason="Phase 13 audit write not implemented yet — RED test from plan 01 (T-13-05)",
strict=True,
)
async def test_upload_success_writes_metadata_only_audit_row(async_client, db_session): async def test_upload_success_writes_metadata_only_audit_row(async_client, db_session):
"""POST upload writes audit row 'cloud.file_uploaded' with metadata only. """POST upload writes audit row 'cloud.file_uploaded' with metadata only.
T-13-05: Upload must be audited. Metadata_ must include filename, size_bytes, T-13-05: Upload must be audited. Metadata_ must include filename and
connection_id, and provider_item_id — never the raw file bytes or provider token. connection_id — never the raw file bytes or provider token.
FAILS: Phase 13 audit write does not exist yet. Plan 06 Task 2 (T-13-20): Implements upload audit write.
""" """
from storage.cloud_base import MUT_KIND_UPLOADED, MUT_REASON_CREATED
from unittest.mock import patch
auth = await _create_user_and_token(db_session) auth = await _create_user_and_token(db_session)
conn = await _create_cloud_connection(db_session, auth["user"].id) conn = await _create_cloud_connection(db_session, auth["user"].id)
new_pid = str(_uuid.uuid4())
mock_adapter = AsyncMock()
mock_adapter.get_object = AsyncMock(return_value=b"fake content")
mock_adapter._normalize_error = MagicMock(return_value={"kind": "error", "reason": "provider_error"})
mock_adapter.upload_file = AsyncMock(return_value={
"kind": MUT_KIND_UPLOADED,
"reason": MUT_REASON_CREATED,
"provider_item_id": new_pid,
"name": "report.pdf",
"parent_ref": "root_audit_test",
"size": 21,
})
files = {"file": ("report.pdf", b"%PDF-1.4 fake content", "application/pdf")} files = {"file": ("report.pdf", b"%PDF-1.4 fake content", "application/pdf")}
data = {"parent_ref": "root", "filename": "report.pdf"} data = {"parent_ref": "root_audit_test_unique", "filename": "report.pdf"}
resp = await async_client.post( with patch("storage.cloud_backend_factory.build_mutable_cloud_adapter", return_value=mock_adapter):
f"/api/cloud/connections/{conn.id}/items/upload", resp = await async_client.post(
headers=auth["headers"], f"/api/cloud/connections/{conn.id}/items/upload",
files=files, headers=auth["headers"],
data=data, files=files,
) data=data,
# 401 means credential decrypt failure in test env; audit check skipped in that case.
assert resp.status_code in (200, 201, 401, 404, 409), (
f"Unexpected status: {resp.status_code}"
)
if resp.status_code in (200, 201):
rows = await _get_recent_audit_rows(
db_session, auth["user"].id, event_type="cloud.file_uploaded"
) )
assert len(rows) >= 1, (
"Successful upload must write audit row 'cloud.file_uploaded' (T-13-05)"
)
row = rows[0]
_assert_metadata_no_secrets(row.metadata_, "cloud.file_uploaded audit")
# Must include metadata about what was uploaded assert resp.status_code == 200, (
assert row.metadata_ is not None f"Expected 200 for upload success, got {resp.status_code}: {resp.text}"
assert "filename" in row.metadata_ or "name" in row.metadata_, ( )
"Upload audit row must include filename or name in metadata_"
) rows = await _get_recent_audit_rows(
# Must NOT include raw file bytes db_session, auth["user"].id, event_type="cloud.file_uploaded"
assert "file_bytes" not in str(row.metadata_), ( )
"Upload audit metadata must never include raw file bytes (T-13-02)" assert len(rows) >= 1, (
) "Successful upload must write audit row 'cloud.file_uploaded' (T-13-05 / T-13-20)"
)
row = rows[0]
_assert_metadata_no_secrets(row.metadata_, "cloud.file_uploaded audit")
# Must include metadata about what was uploaded
assert row.metadata_ is not None
assert "filename" in row.metadata_ or "name" in row.metadata_, (
"Upload audit row must include filename or name in metadata_"
)
# Must NOT include raw file bytes
assert "file_bytes" not in str(row.metadata_), (
"Upload audit metadata must never include raw file bytes (T-13-02)"
)
async def test_upload_conflict_does_not_write_false_overwrite_audit(async_client, db_session): async def test_upload_conflict_does_not_write_false_overwrite_audit(async_client, db_session):