chore: import upstream snapshot with attribution
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for database models."""
|
||||
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
Tests for download tracking models (DownloadTracker, DownloadDuplicates, DownloadAttempt).
|
||||
|
||||
Tests model structure, column constraints, default values, and __repr__ methods
|
||||
WITHOUT requiring a real database -- uses direct attribute assignment on model instances.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Boolean, Integer, String, Text, UniqueConstraint
|
||||
|
||||
from local_deep_research.database.models.download_tracker import (
|
||||
DownloadAttempt,
|
||||
DownloadDuplicates,
|
||||
DownloadTracker,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _col(model_cls, name):
|
||||
"""Return a Column object from a model's __table__."""
|
||||
return model_cls.__table__.columns[name]
|
||||
|
||||
|
||||
def _make(model_cls, **kwargs):
|
||||
"""Instantiate a model via its normal constructor (no DB session needed).
|
||||
|
||||
SQLAlchemy's declarative ``__init__`` accepts column names as keyword
|
||||
arguments and properly initialises the instance state so that
|
||||
instrumented attribute access (used by ``__repr__``) works correctly.
|
||||
"""
|
||||
return model_cls(**kwargs)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DownloadTracker
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestDownloadTrackerTablename:
|
||||
"""Test 1: __tablename__ is 'download_tracker'."""
|
||||
|
||||
def test_tablename(self):
|
||||
assert DownloadTracker.__tablename__ == "download_tracker"
|
||||
|
||||
|
||||
class TestDownloadTrackerColumns:
|
||||
"""Test 2: Column types and nullable constraints."""
|
||||
|
||||
def test_url_column_is_text_and_not_nullable(self):
|
||||
col = _col(DownloadTracker, "url")
|
||||
assert isinstance(col.type, Text)
|
||||
assert col.nullable is False
|
||||
|
||||
def test_url_hash_is_string64_not_nullable_with_table_level_unique(self):
|
||||
col = _col(DownloadTracker, "url_hash")
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 64
|
||||
assert col.nullable is False
|
||||
# Uniqueness lives on a table-level UniqueConstraint named
|
||||
# ``uq_download_tracker_url_hash`` (see model __table_args__) so the
|
||||
# index lands inline in CREATE TABLE — required so SQLCipher accepts
|
||||
# ``url_hash`` as a valid FK target for the child tables. Column-level
|
||||
# ``unique=True``/``index=True`` would emit a separate CREATE UNIQUE
|
||||
# INDEX which SQLCipher does not recognise as the FK target.
|
||||
unique_cols = {
|
||||
tuple(c.name for c in constraint.columns)
|
||||
for constraint in DownloadTracker.__table__.constraints
|
||||
if isinstance(constraint, UniqueConstraint)
|
||||
}
|
||||
assert ("url_hash",) in unique_cols
|
||||
|
||||
def test_first_resource_id_is_integer_not_nullable(self):
|
||||
col = _col(DownloadTracker, "first_resource_id")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is False
|
||||
|
||||
def test_first_resource_id_has_foreign_key(self):
|
||||
col = _col(DownloadTracker, "first_resource_id")
|
||||
fk_targets = [fk.target_fullname for fk in col.foreign_keys]
|
||||
assert "research_resources.id" in fk_targets
|
||||
|
||||
def test_file_hash_is_nullable(self):
|
||||
col = _col(DownloadTracker, "file_hash")
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 64
|
||||
assert col.nullable is True
|
||||
assert col.index is True
|
||||
|
||||
def test_file_path_is_nullable_text(self):
|
||||
col = _col(DownloadTracker, "file_path")
|
||||
assert isinstance(col.type, Text)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_file_name_is_nullable_string255_indexed(self):
|
||||
col = _col(DownloadTracker, "file_name")
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 255
|
||||
assert col.nullable is True
|
||||
assert col.index is True
|
||||
|
||||
def test_file_size_is_nullable_integer(self):
|
||||
col = _col(DownloadTracker, "file_size")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_is_downloaded_is_boolean_not_nullable_indexed(self):
|
||||
col = _col(DownloadTracker, "is_downloaded")
|
||||
assert isinstance(col.type, Boolean)
|
||||
assert col.nullable is False
|
||||
assert col.index is True
|
||||
|
||||
def test_is_accessible_is_boolean(self):
|
||||
col = _col(DownloadTracker, "is_accessible")
|
||||
assert isinstance(col.type, Boolean)
|
||||
|
||||
def test_downloaded_at_is_nullable(self):
|
||||
col = _col(DownloadTracker, "downloaded_at")
|
||||
assert col.nullable is True
|
||||
|
||||
def test_first_seen_is_not_nullable(self):
|
||||
col = _col(DownloadTracker, "first_seen")
|
||||
assert col.nullable is False
|
||||
|
||||
def test_last_checked_is_not_nullable(self):
|
||||
col = _col(DownloadTracker, "last_checked")
|
||||
assert col.nullable is False
|
||||
|
||||
def test_library_document_id_is_nullable_with_fk(self):
|
||||
col = _col(DownloadTracker, "library_document_id")
|
||||
# documents.id is String(36) UUID; column type matches PK type so
|
||||
# SQLite doesn't silently coerce via type-affinity (which produced
|
||||
# the original mismatched-FK bug, see #3697).
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 36
|
||||
assert col.nullable is True
|
||||
fk_targets = [fk.target_fullname for fk in col.foreign_keys]
|
||||
assert "documents.id" in fk_targets
|
||||
|
||||
def test_id_is_primary_key_autoincrement(self):
|
||||
col = _col(DownloadTracker, "id")
|
||||
assert col.primary_key is True
|
||||
assert col.autoincrement is True
|
||||
|
||||
|
||||
class TestDownloadTrackerReprDownloaded:
|
||||
"""Test 3: __repr__ shows 'downloaded' when is_downloaded=True."""
|
||||
|
||||
def test_repr_downloaded(self):
|
||||
obj = _make(
|
||||
DownloadTracker,
|
||||
url_hash="abcdef1234567890",
|
||||
is_downloaded=True,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "downloaded" in result
|
||||
assert "not downloaded" not in result
|
||||
|
||||
|
||||
class TestDownloadTrackerReprNotDownloaded:
|
||||
"""Test 4: __repr__ shows 'not downloaded' when is_downloaded=False."""
|
||||
|
||||
def test_repr_not_downloaded(self):
|
||||
obj = _make(
|
||||
DownloadTracker,
|
||||
url_hash="abcdef1234567890",
|
||||
is_downloaded=False,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "not downloaded" in result
|
||||
|
||||
|
||||
class TestDownloadTrackerReprTruncation:
|
||||
"""Test 5: __repr__ truncates url_hash to first 8 characters."""
|
||||
|
||||
def test_repr_truncates_url_hash(self):
|
||||
full_hash = "a1b2c3d4e5f6g7h8i9j0"
|
||||
obj = _make(
|
||||
DownloadTracker,
|
||||
url_hash=full_hash,
|
||||
is_downloaded=False,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "a1b2c3d4..." in result
|
||||
# The full hash should NOT appear in the repr
|
||||
assert full_hash not in result
|
||||
|
||||
|
||||
class TestDownloadTrackerDefaultIsDownloaded:
|
||||
"""Test 6: Default is_downloaded is False."""
|
||||
|
||||
def test_default_is_downloaded(self):
|
||||
col = _col(DownloadTracker, "is_downloaded")
|
||||
assert col.default is not None
|
||||
assert col.default.arg is False
|
||||
|
||||
|
||||
class TestDownloadTrackerDefaultIsAccessible:
|
||||
"""Test 7: Default is_accessible is True."""
|
||||
|
||||
def test_default_is_accessible(self):
|
||||
col = _col(DownloadTracker, "is_accessible")
|
||||
assert col.default is not None
|
||||
assert col.default.arg is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DownloadDuplicates
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestDownloadDuplicatesTablename:
|
||||
"""Test 8: __tablename__ is 'download_duplicates'."""
|
||||
|
||||
def test_tablename(self):
|
||||
assert DownloadDuplicates.__tablename__ == "download_duplicates"
|
||||
|
||||
|
||||
class TestDownloadDuplicatesUniqueConstraint:
|
||||
"""Test 9: Has UniqueConstraint on (url_hash, resource_id)."""
|
||||
|
||||
def test_unique_constraint_exists(self):
|
||||
table = DownloadDuplicates.__table__
|
||||
unique_constraints = [
|
||||
c
|
||||
for c in table.constraints
|
||||
if c.__class__.__name__ == "UniqueConstraint"
|
||||
]
|
||||
# Find the constraint named 'uix_url_resource'
|
||||
matching = [
|
||||
c for c in unique_constraints if c.name == "uix_url_resource"
|
||||
]
|
||||
assert len(matching) == 1
|
||||
col_names = {col.name for col in matching[0].columns}
|
||||
assert col_names == {"url_hash", "resource_id"}
|
||||
|
||||
|
||||
class TestDownloadDuplicatesRepr:
|
||||
"""Test 10: __repr__ shows url_hash and resource_id."""
|
||||
|
||||
def test_repr_content(self):
|
||||
obj = _make(
|
||||
DownloadDuplicates,
|
||||
url_hash="deadbeef12345678",
|
||||
resource_id=42,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "deadbeef..." in result
|
||||
assert "resource_id=42" in result
|
||||
assert "DownloadDuplicates" in result
|
||||
|
||||
|
||||
class TestDownloadDuplicatesCompositeIndex:
|
||||
"""Test 11: Has composite index on (research_id, url_hash)."""
|
||||
|
||||
def test_composite_index_exists(self):
|
||||
table = DownloadDuplicates.__table__
|
||||
index_names = {idx.name for idx in table.indexes}
|
||||
assert "idx_research_duplicates" in index_names
|
||||
|
||||
# Verify the columns in the index
|
||||
target_index = next(
|
||||
idx
|
||||
for idx in table.indexes
|
||||
if idx.name == "idx_research_duplicates"
|
||||
)
|
||||
col_names = [col.name for col in target_index.columns]
|
||||
assert "research_id" in col_names
|
||||
assert "url_hash" in col_names
|
||||
|
||||
|
||||
class TestDownloadDuplicatesColumns:
|
||||
"""Additional column validation for DownloadDuplicates."""
|
||||
|
||||
def test_url_hash_not_nullable_with_fk(self):
|
||||
col = _col(DownloadDuplicates, "url_hash")
|
||||
assert col.nullable is False
|
||||
fk_targets = [fk.target_fullname for fk in col.foreign_keys]
|
||||
assert "download_tracker.url_hash" in fk_targets
|
||||
|
||||
def test_resource_id_not_nullable_with_fk(self):
|
||||
col = _col(DownloadDuplicates, "resource_id")
|
||||
assert col.nullable is False
|
||||
fk_targets = [fk.target_fullname for fk in col.foreign_keys]
|
||||
assert "research_resources.id" in fk_targets
|
||||
|
||||
def test_research_id_not_nullable_indexed(self):
|
||||
col = _col(DownloadDuplicates, "research_id")
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 36
|
||||
assert col.nullable is False
|
||||
assert col.index is True
|
||||
|
||||
def test_added_at_not_nullable(self):
|
||||
col = _col(DownloadDuplicates, "added_at")
|
||||
assert col.nullable is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DownloadAttempt
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestDownloadAttemptTablename:
|
||||
"""Test 12: __tablename__ is 'download_attempts'."""
|
||||
|
||||
def test_tablename(self):
|
||||
assert DownloadAttempt.__tablename__ == "download_attempts"
|
||||
|
||||
|
||||
class TestDownloadAttemptReprSuccess:
|
||||
"""Test 13: __repr__ shows 'success' when succeeded=True."""
|
||||
|
||||
def test_repr_success(self):
|
||||
obj = _make(
|
||||
DownloadAttempt,
|
||||
attempt_number=1,
|
||||
succeeded=True,
|
||||
status_code=200,
|
||||
error_type=None,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "success" in result
|
||||
assert "failed" not in result
|
||||
assert "attempt=1" in result
|
||||
|
||||
|
||||
class TestDownloadAttemptReprFailedWithStatusCode:
|
||||
"""Test 14: __repr__ shows 'failed (404)' when succeeded=False, status_code=404."""
|
||||
|
||||
def test_repr_failed_status_code(self):
|
||||
obj = _make(
|
||||
DownloadAttempt,
|
||||
attempt_number=2,
|
||||
succeeded=False,
|
||||
status_code=404,
|
||||
error_type=None,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "failed (404)" in result
|
||||
assert "attempt=2" in result
|
||||
|
||||
|
||||
class TestDownloadAttemptReprFailedWithErrorType:
|
||||
"""Test 15: __repr__ shows 'failed (timeout)' when no status_code, error_type='timeout'."""
|
||||
|
||||
def test_repr_failed_error_type(self):
|
||||
obj = _make(
|
||||
DownloadAttempt,
|
||||
attempt_number=3,
|
||||
succeeded=False,
|
||||
status_code=None,
|
||||
error_type="timeout",
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "failed (timeout)" in result
|
||||
assert "attempt=3" in result
|
||||
|
||||
|
||||
class TestDownloadAttemptReprFailedNoInfo:
|
||||
"""Test 16: __repr__ shows 'failed (None)' when no status_code or error_type."""
|
||||
|
||||
def test_repr_failed_none(self):
|
||||
obj = _make(
|
||||
DownloadAttempt,
|
||||
attempt_number=1,
|
||||
succeeded=False,
|
||||
status_code=None,
|
||||
error_type=None,
|
||||
)
|
||||
result = repr(obj)
|
||||
assert "failed (None)" in result
|
||||
|
||||
|
||||
class TestDownloadAttemptDefaultSucceeded:
|
||||
"""Test 17: Default succeeded is False."""
|
||||
|
||||
def test_default_succeeded(self):
|
||||
col = _col(DownloadAttempt, "succeeded")
|
||||
assert col.default is not None
|
||||
assert col.default.arg is False
|
||||
|
||||
|
||||
class TestDownloadAttemptColumns:
|
||||
"""Additional column validation for DownloadAttempt."""
|
||||
|
||||
def test_url_hash_not_nullable_with_fk(self):
|
||||
col = _col(DownloadAttempt, "url_hash")
|
||||
assert col.nullable is False
|
||||
assert col.index is True
|
||||
fk_targets = [fk.target_fullname for fk in col.foreign_keys]
|
||||
assert "download_tracker.url_hash" in fk_targets
|
||||
|
||||
def test_attempt_number_not_nullable(self):
|
||||
col = _col(DownloadAttempt, "attempt_number")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is False
|
||||
|
||||
def test_status_code_nullable(self):
|
||||
col = _col(DownloadAttempt, "status_code")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_error_type_nullable_string100(self):
|
||||
col = _col(DownloadAttempt, "error_type")
|
||||
assert isinstance(col.type, String)
|
||||
assert col.type.length == 100
|
||||
assert col.nullable is True
|
||||
|
||||
def test_error_message_nullable_text(self):
|
||||
col = _col(DownloadAttempt, "error_message")
|
||||
assert isinstance(col.type, Text)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_attempted_at_not_nullable(self):
|
||||
col = _col(DownloadAttempt, "attempted_at")
|
||||
assert col.nullable is False
|
||||
|
||||
def test_duration_ms_nullable(self):
|
||||
col = _col(DownloadAttempt, "duration_ms")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_succeeded_not_nullable(self):
|
||||
col = _col(DownloadAttempt, "succeeded")
|
||||
assert isinstance(col.type, Boolean)
|
||||
assert col.nullable is False
|
||||
|
||||
def test_bytes_downloaded_nullable(self):
|
||||
col = _col(DownloadAttempt, "bytes_downloaded")
|
||||
assert isinstance(col.type, Integer)
|
||||
assert col.nullable is True
|
||||
|
||||
def test_id_primary_key(self):
|
||||
col = _col(DownloadAttempt, "id")
|
||||
assert col.primary_key is True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,770 @@
|
||||
"""
|
||||
Extended tests for library models - Comprehensive coverage of unified document architecture.
|
||||
|
||||
Tests cover:
|
||||
- Document model operations
|
||||
- Collection model operations
|
||||
- DocumentCollection (many-to-many) operations
|
||||
- DocumentChunk model operations
|
||||
- DownloadQueue model operations
|
||||
- RAGIndex model operations
|
||||
- LibraryStatistics model operations
|
||||
- CollectionFolder and CollectionFolderFile models
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from local_deep_research.database.models import Base
|
||||
from local_deep_research.database.models.library import (
|
||||
Document,
|
||||
DocumentBlob,
|
||||
Collection,
|
||||
DocumentCollection,
|
||||
DocumentChunk,
|
||||
DownloadQueue,
|
||||
LibraryStatistics,
|
||||
RAGIndex,
|
||||
CollectionFolder,
|
||||
CollectionFolderFile,
|
||||
SourceType,
|
||||
UploadBatch,
|
||||
DocumentStatus,
|
||||
RAGIndexStatus,
|
||||
EmbeddingProvider,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine():
|
||||
"""Create in-memory SQLite engine."""
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
yield engine
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session(engine):
|
||||
"""Create database session."""
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def source_type(session):
|
||||
"""Create a source type for testing."""
|
||||
st = SourceType(
|
||||
id=str(uuid.uuid4()),
|
||||
name="research_download",
|
||||
display_name="Research Download",
|
||||
description="Downloaded from research sources",
|
||||
)
|
||||
session.add(st)
|
||||
session.commit()
|
||||
return st
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def collection(session):
|
||||
"""Create a collection for testing."""
|
||||
coll = Collection(
|
||||
id=str(uuid.uuid4()),
|
||||
name="Test Collection",
|
||||
description="A test collection",
|
||||
collection_type="user_collection",
|
||||
is_default=False,
|
||||
)
|
||||
session.add(coll)
|
||||
session.commit()
|
||||
return coll
|
||||
|
||||
|
||||
class TestDocumentModel:
|
||||
"""Tests for Document model."""
|
||||
|
||||
def test_create_document(self, session, source_type):
|
||||
"""Should create a document."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="abc123def456" * 5 + "ab", # 64 chars
|
||||
file_size=1024,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
assert doc.id is not None
|
||||
assert doc.file_size == 1024
|
||||
|
||||
def test_document_with_all_fields(self, session, source_type):
|
||||
"""Should create document with all optional fields."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="xyz789" * 10 + "abcd",
|
||||
file_size=2048,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
title="Test Paper",
|
||||
description="A test paper description",
|
||||
authors=["Author One", "Author Two"],
|
||||
doi="10.1234/test.doi",
|
||||
arxiv_id="2301.00001",
|
||||
text_content="This is the paper content...",
|
||||
extraction_method="pdf_extraction",
|
||||
extraction_source="pdfplumber",
|
||||
extraction_quality="high",
|
||||
tags=["machine-learning", "nlp"],
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
retrieved = session.query(Document).filter_by(id=doc.id).first()
|
||||
assert retrieved.title == "Test Paper"
|
||||
assert retrieved.authors == ["Author One", "Author Two"]
|
||||
|
||||
def test_document_status_enum(self, session, source_type):
|
||||
"""Document status should use enum values."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="status123" * 7 + "a",
|
||||
file_size=512,
|
||||
file_type="txt",
|
||||
status=DocumentStatus.PENDING,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
assert doc.status == DocumentStatus.PENDING
|
||||
|
||||
def test_document_unique_hash(self, session, source_type):
|
||||
"""Document hash should be unique."""
|
||||
hash_value = "unique_hash" * 5 + "abcd"
|
||||
|
||||
doc1 = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash=hash_value,
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc1)
|
||||
session.commit()
|
||||
|
||||
doc2 = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash=hash_value, # Same hash
|
||||
file_size=200,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc2)
|
||||
|
||||
with pytest.raises(Exception): # IntegrityError
|
||||
session.commit()
|
||||
|
||||
def test_document_repr(self, session, source_type):
|
||||
"""Document __repr__ should work."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="repr_test" * 8,
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
title="Repr Test",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
repr_str = repr(doc)
|
||||
assert "Document" in repr_str
|
||||
|
||||
|
||||
class TestCollectionModel:
|
||||
"""Tests for Collection model."""
|
||||
|
||||
def test_create_collection(self, session):
|
||||
"""Should create a collection."""
|
||||
coll = Collection(
|
||||
id=str(uuid.uuid4()),
|
||||
name="My Collection",
|
||||
description="Test description",
|
||||
)
|
||||
session.add(coll)
|
||||
session.commit()
|
||||
|
||||
assert coll.id is not None
|
||||
assert coll.name == "My Collection"
|
||||
|
||||
def test_collection_default_values(self, session):
|
||||
"""Collection should have correct defaults."""
|
||||
coll = Collection(
|
||||
id=str(uuid.uuid4()),
|
||||
name="Default Test",
|
||||
)
|
||||
session.add(coll)
|
||||
session.commit()
|
||||
|
||||
assert coll.is_default is False
|
||||
assert coll.collection_type == "user_collection"
|
||||
|
||||
def test_collection_with_embedding_config(self, session):
|
||||
"""Collection can store embedding configuration."""
|
||||
coll = Collection(
|
||||
id=str(uuid.uuid4()),
|
||||
name="Embedding Collection",
|
||||
embedding_model="all-MiniLM-L6-v2",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
embedding_dimension=384,
|
||||
chunk_size=512,
|
||||
chunk_overlap=50,
|
||||
)
|
||||
session.add(coll)
|
||||
session.commit()
|
||||
|
||||
retrieved = session.query(Collection).filter_by(id=coll.id).first()
|
||||
assert retrieved.embedding_model == "all-MiniLM-L6-v2"
|
||||
assert retrieved.embedding_dimension == 384
|
||||
|
||||
def test_collection_repr(self, session):
|
||||
"""Collection __repr__ should work."""
|
||||
coll = Collection(
|
||||
id="test-id",
|
||||
name="Repr Test",
|
||||
collection_type="user_collection",
|
||||
)
|
||||
repr_str = repr(coll)
|
||||
assert "Collection" in repr_str
|
||||
|
||||
|
||||
class TestDocumentCollectionModel:
|
||||
"""Tests for DocumentCollection many-to-many model."""
|
||||
|
||||
def test_link_document_to_collection(
|
||||
self, session, source_type, collection
|
||||
):
|
||||
"""Should link document to collection."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="link_test" * 8,
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
link = DocumentCollection(
|
||||
document_id=doc.id,
|
||||
collection_id=collection.id,
|
||||
indexed=False,
|
||||
chunk_count=0,
|
||||
)
|
||||
session.add(link)
|
||||
session.commit()
|
||||
|
||||
assert link.id is not None
|
||||
|
||||
def test_document_collection_unique_pair(
|
||||
self, session, source_type, collection
|
||||
):
|
||||
"""Document-collection pair should be unique."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="unique_pair" * 6 + "ab",
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
link1 = DocumentCollection(
|
||||
document_id=doc.id,
|
||||
collection_id=collection.id,
|
||||
)
|
||||
session.add(link1)
|
||||
session.commit()
|
||||
|
||||
link2 = DocumentCollection(
|
||||
document_id=doc.id,
|
||||
collection_id=collection.id, # Same pair
|
||||
)
|
||||
session.add(link2)
|
||||
|
||||
with pytest.raises(Exception): # IntegrityError
|
||||
session.commit()
|
||||
|
||||
def test_document_collection_indexing_status(
|
||||
self, session, source_type, collection
|
||||
):
|
||||
"""Should track indexing status per collection."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="index_status" * 6 + "12",
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
link = DocumentCollection(
|
||||
document_id=doc.id,
|
||||
collection_id=collection.id,
|
||||
indexed=True,
|
||||
chunk_count=25,
|
||||
)
|
||||
session.add(link)
|
||||
session.commit()
|
||||
|
||||
retrieved = (
|
||||
session.query(DocumentCollection)
|
||||
.filter_by(document_id=doc.id)
|
||||
.first()
|
||||
)
|
||||
assert retrieved.indexed is True
|
||||
assert retrieved.chunk_count == 25
|
||||
|
||||
|
||||
class TestDocumentChunkModel:
|
||||
"""Tests for DocumentChunk model."""
|
||||
|
||||
def test_create_document_chunk(self, session):
|
||||
"""Should create a document chunk."""
|
||||
chunk = DocumentChunk(
|
||||
chunk_hash="chunk_hash" * 6 + "ab",
|
||||
source_type="document",
|
||||
source_id=str(uuid.uuid4()),
|
||||
collection_name="collection_abc123",
|
||||
chunk_text="This is the chunk text content.",
|
||||
chunk_index=0,
|
||||
start_char=0,
|
||||
end_char=31,
|
||||
word_count=6,
|
||||
embedding_id=str(uuid.uuid4()),
|
||||
embedding_model="all-MiniLM-L6-v2",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
)
|
||||
session.add(chunk)
|
||||
session.commit()
|
||||
|
||||
assert chunk.id is not None
|
||||
|
||||
def test_chunk_unique_per_collection(self, session):
|
||||
"""Chunk hash should be unique per collection."""
|
||||
chunk_hash = "duplicate_hash" * 5
|
||||
|
||||
chunk1 = DocumentChunk(
|
||||
chunk_hash=chunk_hash,
|
||||
source_type="document",
|
||||
collection_name="collection_1",
|
||||
chunk_text="Content 1",
|
||||
chunk_index=0,
|
||||
start_char=0,
|
||||
end_char=10,
|
||||
word_count=2,
|
||||
embedding_id=str(uuid.uuid4()),
|
||||
embedding_model="model",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
)
|
||||
session.add(chunk1)
|
||||
session.commit()
|
||||
|
||||
# Same hash, same collection should fail
|
||||
chunk2 = DocumentChunk(
|
||||
chunk_hash=chunk_hash,
|
||||
source_type="document",
|
||||
collection_name="collection_1", # Same collection
|
||||
chunk_text="Content 2",
|
||||
chunk_index=1,
|
||||
start_char=10,
|
||||
end_char=20,
|
||||
word_count=2,
|
||||
embedding_id=str(uuid.uuid4()),
|
||||
embedding_model="model",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
)
|
||||
session.add(chunk2)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
session.commit()
|
||||
|
||||
def test_chunk_repr(self, session):
|
||||
"""DocumentChunk __repr__ should work."""
|
||||
chunk = DocumentChunk(
|
||||
chunk_hash="repr_test" * 8,
|
||||
source_type="document",
|
||||
collection_name="test_collection",
|
||||
chunk_text="Test content",
|
||||
chunk_index=5,
|
||||
start_char=100,
|
||||
end_char=200,
|
||||
word_count=10,
|
||||
embedding_id=str(uuid.uuid4()),
|
||||
embedding_model="model",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
)
|
||||
repr_str = repr(chunk)
|
||||
assert "DocumentChunk" in repr_str
|
||||
|
||||
|
||||
class TestRAGIndexModel:
|
||||
"""Tests for RAGIndex model."""
|
||||
|
||||
def test_create_rag_index(self, session):
|
||||
"""Should create a RAG index."""
|
||||
index = RAGIndex(
|
||||
collection_name="collection_abc",
|
||||
embedding_model="all-MiniLM-L6-v2",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
embedding_dimension=384,
|
||||
index_path="/data/indexes/collection_abc.faiss",
|
||||
index_hash="index_hash" * 6 + "ab",
|
||||
chunk_size=512,
|
||||
chunk_overlap=50,
|
||||
status=RAGIndexStatus.ACTIVE,
|
||||
)
|
||||
session.add(index)
|
||||
session.commit()
|
||||
|
||||
assert index.id is not None
|
||||
|
||||
def test_rag_index_status_transitions(self, session):
|
||||
"""RAG index status can transition."""
|
||||
index = RAGIndex(
|
||||
collection_name="status_test",
|
||||
embedding_model="model",
|
||||
embedding_model_type=EmbeddingProvider.OLLAMA,
|
||||
embedding_dimension=768,
|
||||
index_path="/path/to/index.faiss",
|
||||
index_hash="status_hash" * 6 + "ab",
|
||||
chunk_size=256,
|
||||
chunk_overlap=25,
|
||||
status=RAGIndexStatus.ACTIVE,
|
||||
)
|
||||
session.add(index)
|
||||
session.commit()
|
||||
|
||||
# Update status
|
||||
index.status = RAGIndexStatus.REBUILDING
|
||||
session.commit()
|
||||
|
||||
retrieved = session.query(RAGIndex).filter_by(id=index.id).first()
|
||||
assert retrieved.status == RAGIndexStatus.REBUILDING
|
||||
|
||||
def test_rag_index_repr(self, session):
|
||||
"""RAGIndex __repr__ should work."""
|
||||
index = RAGIndex(
|
||||
collection_name="repr_collection",
|
||||
embedding_model="test-model",
|
||||
embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS,
|
||||
embedding_dimension=384,
|
||||
index_path="/path/index.faiss",
|
||||
index_hash="repr_hash" * 8,
|
||||
chunk_size=512,
|
||||
chunk_overlap=50,
|
||||
chunk_count=100,
|
||||
)
|
||||
repr_str = repr(index)
|
||||
assert "RAGIndex" in repr_str
|
||||
|
||||
|
||||
class TestLibraryStatisticsModel:
|
||||
"""Tests for LibraryStatistics model."""
|
||||
|
||||
def test_create_statistics(self, session):
|
||||
"""Should create library statistics."""
|
||||
stats = LibraryStatistics(
|
||||
total_documents=100,
|
||||
total_pdfs=80,
|
||||
total_html=15,
|
||||
total_other=5,
|
||||
total_size_bytes=1024000,
|
||||
average_document_size=10240,
|
||||
)
|
||||
session.add(stats)
|
||||
session.commit()
|
||||
|
||||
assert stats.id is not None
|
||||
|
||||
def test_statistics_download_metrics(self, session):
|
||||
"""Statistics should track download metrics."""
|
||||
stats = LibraryStatistics(
|
||||
total_documents=50,
|
||||
total_download_attempts=100,
|
||||
successful_downloads=45,
|
||||
failed_downloads=5,
|
||||
pending_downloads=50,
|
||||
)
|
||||
session.add(stats)
|
||||
session.commit()
|
||||
|
||||
retrieved = (
|
||||
session.query(LibraryStatistics).filter_by(id=stats.id).first()
|
||||
)
|
||||
assert retrieved.total_download_attempts == 100
|
||||
assert retrieved.successful_downloads == 45
|
||||
|
||||
def test_statistics_repr(self, session):
|
||||
"""LibraryStatistics __repr__ should work."""
|
||||
stats = LibraryStatistics(
|
||||
total_documents=50,
|
||||
total_size_bytes=500000,
|
||||
)
|
||||
repr_str = repr(stats)
|
||||
assert "LibraryStatistics" in repr_str
|
||||
|
||||
|
||||
class TestDownloadQueueModel:
|
||||
"""Tests for DownloadQueue model."""
|
||||
|
||||
def test_create_queue_item(self, session, collection):
|
||||
"""Should create a download queue item."""
|
||||
# Note: This requires a ResearchResource to exist
|
||||
# For now, test the model structure
|
||||
queue = DownloadQueue.__table__
|
||||
columns = {c.name for c in queue.columns}
|
||||
|
||||
assert "resource_id" in columns
|
||||
assert "research_id" in columns
|
||||
assert "priority" in columns
|
||||
assert "status" in columns
|
||||
assert "attempts" in columns
|
||||
|
||||
|
||||
class TestCollectionFolderModel:
|
||||
"""Tests for CollectionFolder model."""
|
||||
|
||||
def test_create_collection_folder(self, session, collection):
|
||||
"""Should create a collection folder link."""
|
||||
folder = CollectionFolder(
|
||||
collection_id=collection.id,
|
||||
folder_path="/home/user/documents/research",
|
||||
include_patterns=["*.pdf", "*.txt"],
|
||||
recursive=True,
|
||||
)
|
||||
session.add(folder)
|
||||
session.commit()
|
||||
|
||||
assert folder.id is not None
|
||||
|
||||
def test_folder_default_patterns(self, session, collection):
|
||||
"""Folder should have default include patterns."""
|
||||
folder = CollectionFolder(
|
||||
collection_id=collection.id,
|
||||
folder_path="/path/to/folder",
|
||||
)
|
||||
session.add(folder)
|
||||
session.commit()
|
||||
|
||||
# Default patterns should include common document types
|
||||
assert folder.include_patterns is not None
|
||||
|
||||
def test_folder_repr(self, session, collection):
|
||||
"""CollectionFolder __repr__ should work."""
|
||||
folder = CollectionFolder(
|
||||
collection_id=collection.id,
|
||||
folder_path="/test/path",
|
||||
file_count=10,
|
||||
)
|
||||
repr_str = repr(folder)
|
||||
assert "CollectionFolder" in repr_str
|
||||
|
||||
|
||||
class TestCollectionFolderFileModel:
|
||||
"""Tests for CollectionFolderFile model."""
|
||||
|
||||
def test_create_folder_file(self, session, collection):
|
||||
"""Should create a folder file entry."""
|
||||
folder = CollectionFolder(
|
||||
collection_id=collection.id,
|
||||
folder_path="/test/folder",
|
||||
)
|
||||
session.add(folder)
|
||||
session.commit()
|
||||
|
||||
file = CollectionFolderFile(
|
||||
folder_id=folder.id,
|
||||
relative_path="subdir/document.pdf",
|
||||
file_hash="file_hash" * 8,
|
||||
file_size=2048,
|
||||
file_type="pdf",
|
||||
indexed=False,
|
||||
)
|
||||
session.add(file)
|
||||
session.commit()
|
||||
|
||||
assert file.id is not None
|
||||
|
||||
def test_folder_file_unique_path(self, session, collection):
|
||||
"""File path should be unique within folder."""
|
||||
folder = CollectionFolder(
|
||||
collection_id=collection.id,
|
||||
folder_path="/unique/test",
|
||||
)
|
||||
session.add(folder)
|
||||
session.commit()
|
||||
|
||||
file1 = CollectionFolderFile(
|
||||
folder_id=folder.id,
|
||||
relative_path="same/path.pdf",
|
||||
)
|
||||
session.add(file1)
|
||||
session.commit()
|
||||
|
||||
file2 = CollectionFolderFile(
|
||||
folder_id=folder.id,
|
||||
relative_path="same/path.pdf", # Same path
|
||||
)
|
||||
session.add(file2)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
session.commit()
|
||||
|
||||
def test_folder_file_repr(self):
|
||||
"""CollectionFolderFile __repr__ should work."""
|
||||
file = CollectionFolderFile(
|
||||
relative_path="test/file.pdf",
|
||||
indexed=True,
|
||||
)
|
||||
repr_str = repr(file)
|
||||
assert "CollectionFolderFile" in repr_str
|
||||
|
||||
|
||||
class TestSourceTypeModel:
|
||||
"""Tests for SourceType model."""
|
||||
|
||||
def test_create_source_type(self, session):
|
||||
"""Should create a source type."""
|
||||
st = SourceType(
|
||||
id=str(uuid.uuid4()),
|
||||
name="user_upload",
|
||||
display_name="User Upload",
|
||||
description="Uploaded by user",
|
||||
icon="upload",
|
||||
)
|
||||
session.add(st)
|
||||
session.commit()
|
||||
|
||||
assert st.id is not None
|
||||
|
||||
def test_source_type_unique_name(self, session):
|
||||
"""Source type name should be unique."""
|
||||
st1 = SourceType(
|
||||
id=str(uuid.uuid4()),
|
||||
name="unique_type",
|
||||
display_name="Unique Type",
|
||||
)
|
||||
session.add(st1)
|
||||
session.commit()
|
||||
|
||||
st2 = SourceType(
|
||||
id=str(uuid.uuid4()),
|
||||
name="unique_type", # Same name
|
||||
display_name="Another Unique Type",
|
||||
)
|
||||
session.add(st2)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
session.commit()
|
||||
|
||||
def test_source_type_repr(self):
|
||||
"""SourceType __repr__ should work."""
|
||||
st = SourceType(
|
||||
id="test-id",
|
||||
name="test_type",
|
||||
display_name="Test Type",
|
||||
)
|
||||
repr_str = repr(st)
|
||||
assert "SourceType" in repr_str
|
||||
|
||||
|
||||
class TestDocumentBlobModel:
|
||||
"""Tests for DocumentBlob model."""
|
||||
|
||||
def test_create_document_blob(self, session, source_type):
|
||||
"""Should create a document blob."""
|
||||
doc = Document(
|
||||
id=str(uuid.uuid4()),
|
||||
source_type_id=source_type.id,
|
||||
document_hash="blob_test" * 8,
|
||||
file_size=1000,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
session.add(doc)
|
||||
session.commit()
|
||||
|
||||
blob = DocumentBlob(
|
||||
document_id=doc.id,
|
||||
pdf_binary=b"PDF binary content here",
|
||||
blob_hash="binary_hash" * 6 + "ab",
|
||||
)
|
||||
session.add(blob)
|
||||
session.commit()
|
||||
|
||||
retrieved = (
|
||||
session.query(DocumentBlob).filter_by(document_id=doc.id).first()
|
||||
)
|
||||
assert retrieved.pdf_binary == b"PDF binary content here"
|
||||
|
||||
def test_blob_repr(self, session, source_type):
|
||||
"""DocumentBlob __repr__ should work."""
|
||||
doc = Document(
|
||||
id="test-doc-id-" + "x" * 24,
|
||||
source_type_id=source_type.id,
|
||||
document_hash="repr_blob" * 8,
|
||||
file_size=100,
|
||||
file_type="pdf",
|
||||
status=DocumentStatus.COMPLETED,
|
||||
)
|
||||
blob = DocumentBlob(
|
||||
document_id=doc.id,
|
||||
pdf_binary=b"test",
|
||||
)
|
||||
repr_str = repr(blob)
|
||||
assert "DocumentBlob" in repr_str
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:UploadBatch is deprecated.*:DeprecationWarning"
|
||||
)
|
||||
class TestUploadBatchModel:
|
||||
"""Tests for UploadBatch model (deprecated — see PR 11a)."""
|
||||
|
||||
def test_create_upload_batch(self, session, collection):
|
||||
"""Should create an upload batch."""
|
||||
batch = UploadBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
collection_id=collection.id,
|
||||
file_count=5,
|
||||
total_size=10240,
|
||||
)
|
||||
session.add(batch)
|
||||
session.commit()
|
||||
|
||||
assert batch.id is not None
|
||||
assert batch.file_count == 5
|
||||
|
||||
def test_batch_repr(self):
|
||||
"""UploadBatch __repr__ should work."""
|
||||
batch = UploadBatch(
|
||||
id="test-batch-id",
|
||||
file_count=3,
|
||||
total_size=5000,
|
||||
)
|
||||
repr_str = repr(batch)
|
||||
assert "UploadBatch" in repr_str
|
||||
Reference in New Issue
Block a user