feat(files): add source-file upload with MinIO storage and Postgres repositories

Why:
- Implements plan 001 Phase 3's upload orchestration.

Changes:
- ObjectStorage port and MinIO adapter, thread-offloaded per ADR-0017.
- Tenant-scoped repositories for api_keys, source_files, ingestion_jobs.
- upload_source_file implementing the two-transaction shape with
  (tenant_id, domain, content_sha256) idempotency.

Impact:
- This phase stores bytes only -- chunks_indexed is always 0 until
  Phase 4/5 add parsing/embedding.
This commit is contained in:
2026-08-19 15:00:27 +03:30
parent e97ce6e5f3
commit c9cf7b368b
19 changed files with 758 additions and 4 deletions

View File

@@ -0,0 +1,58 @@
import pytest
from src.application.files.errors import FileTooLargeError, InvalidUploadError
from src.application.files.validation import validate_and_hash_upload
from src.application.ingestion.errors import UnsupportedSourceTypeError
pytestmark = pytest.mark.unit
_DOCX_HEADER = b"PK\x03\x04" + b"\x00" * 20
_CSV_BYTES = b"name,value\nfirst,1\n"
def test_validate_and_hash_upload_accepts_matching_csv() -> None:
result = validate_and_hash_upload(filename="report.csv", data=_CSV_BYTES, max_size_bytes=1_000)
assert result.source_type == "csv"
assert result.content_type == "text/csv"
assert len(result.content_sha256) == 64
def test_validate_and_hash_upload_accepts_matching_docx() -> None:
result = validate_and_hash_upload(
filename="report.docx", data=_DOCX_HEADER, max_size_bytes=1_000
)
assert result.source_type == "docx"
def test_validate_and_hash_upload_rejects_doc_as_unsupported() -> None:
with pytest.raises(UnsupportedSourceTypeError):
validate_and_hash_upload(filename="legacy.doc", data=_CSV_BYTES, max_size_bytes=1_000)
def test_validate_and_hash_upload_rejects_unknown_extension() -> None:
with pytest.raises(UnsupportedSourceTypeError):
validate_and_hash_upload(filename="report.pdf", data=_CSV_BYTES, max_size_bytes=1_000)
def test_validate_and_hash_upload_rejects_empty_file() -> None:
with pytest.raises(InvalidUploadError):
validate_and_hash_upload(filename="report.csv", data=b"", max_size_bytes=1_000)
def test_validate_and_hash_upload_rejects_oversized_file() -> None:
with pytest.raises(FileTooLargeError):
validate_and_hash_upload(filename="report.csv", data=_CSV_BYTES, max_size_bytes=4)
def test_validate_and_hash_upload_rejects_spoofed_docx_extension() -> None:
"""Content is really CSV text, but the filename claims `.docx`."""
with pytest.raises(InvalidUploadError):
validate_and_hash_upload(filename="report.docx", data=_CSV_BYTES, max_size_bytes=1_000)
def test_validate_and_hash_upload_rejects_spoofed_csv_extension() -> None:
"""Content is really an OOXML zip, but the filename claims `.csv`."""
with pytest.raises(InvalidUploadError):
validate_and_hash_upload(filename="report.csv", data=_DOCX_HEADER, max_size_bytes=1_000)