test(phase-1): add Nyquist validation tests — STORE-07 concurrent put, fix confirm UUID

- Add test_concurrent_put_objects to test_storage.py (STORE-07: verifies no
  per-instance lock blocks concurrent MinIO workers via asyncio.gather)
- Remove @pytest.mark.xfail from test_confirm_endpoint; test now passes on
  SQLite after uuid format fix in api/documents.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-30 18:56:58 +02:00
parent 33e5efe846
commit bd765f69bf
2 changed files with 72 additions and 1 deletions
-1
View File
@@ -199,7 +199,6 @@ async def test_upload_url_endpoint(async_client, auth_user, mock_minio_presigned
assert mock_minio_presigned.called, "generate_presigned_put_url was not called"
@pytest.mark.xfail(strict=False, reason="SQLite UUID format mismatch in raw SQL quota UPDATE — xpass on PostgreSQL (INTEGRATION=1)")
async def test_confirm_endpoint(
async_client, auth_user, mock_minio_presigned, mock_minio_stat, monkeypatch
):
+72
View File
@@ -203,3 +203,75 @@ async def test_minio_backend_health_check_returns_bool():
result2 = await backend2.health_check()
assert result2 is False, f"Expected False on exception, got {result2!r}"
# ---------------------------------------------------------------------------
# Test 7: STORE-07 — no file locks; concurrent put_object calls both complete
# ---------------------------------------------------------------------------
async def test_concurrent_put_objects():
"""STORE-07: Two concurrent put_object calls must both complete without error
and return distinct object keys.
This proves there is no shared mutable per-instance lock that would cause
one coroutine to block or fail while the other holds a resource. A naive
implementation that uses a threading.Lock or asyncio.Lock around the entire
put_object body would serialize the calls; a correct async implementation
using asyncio.to_thread does not block other coroutines.
"""
try:
from storage.minio_backend import MinIOBackend
except ImportError as exc:
pytest.skip(f"{exc}")
import asyncio
from unittest.mock import MagicMock
backend = MinIOBackend.__new__(MinIOBackend)
backend._client = MagicMock()
backend._bucket = "docuvault"
backend._client.put_object = MagicMock(return_value=None)
user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
document_id_1 = "11111111-1111-1111-1111-111111111111"
document_id_2 = "22222222-2222-2222-2222-222222222222"
key1, key2 = await asyncio.gather(
backend.put_object(
user_id=user_id,
document_id=document_id_1,
file_bytes=b"first file content",
extension=".txt",
content_type="text/plain",
),
backend.put_object(
user_id=user_id,
document_id=document_id_2,
file_bytes=b"second file content",
extension=".pdf",
content_type="application/pdf",
),
)
# Both calls must have returned a non-empty string key
assert key1 and isinstance(key1, str), f"First put_object returned invalid key: {key1!r}"
assert key2 and isinstance(key2, str), f"Second put_object returned invalid key: {key2!r}"
# Keys must be distinct — they embed a uuid4() per call
assert key1 != key2, (
f"Concurrent put_object calls returned the same key: {key1!r}. "
"This indicates a shared mutable state bug (e.g., a global counter or lock)."
)
# Both keys must follow the STORE-02 schema
pattern = re.compile(
r'^[^/]+/[^/]+/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(\.[a-zA-Z0-9]+)?$'
)
assert pattern.match(key1), f"key1 '{key1}' does not match STORE-02 schema"
assert pattern.match(key2), f"key2 '{key2}' does not match STORE-02 schema"
# sdk put_object must have been called exactly twice (one per concurrent call)
assert backend._client.put_object.call_count == 2, (
f"Expected 2 put_object SDK calls for 2 concurrent uploads, "
f"got {backend._client.put_object.call_count}"
)