From 5d1d2c6c44e7cac02f0836709bd3742d13ec29b6 Mon Sep 17 00:00:00 2001 From: Sanchit Ram Arvind Date: Thu, 2 Jan 2025 20:41:48 -0600 Subject: [PATCH] ci (#47) * crates tests * +ci * add paths --- .github/workflows/ci.yml | 39 +++ core/models/__init__.py | 10 +- pytest.ini | 15 + tests/conftest.py | 146 ++++++++++ tests/requirements.txt | 5 + tests/system/test_pipeline.py | 85 ++++++ tests/unit/test_crates_transformer.py | 250 ++++++++++++++++ tests/unit/test_db_models.py | 400 ++++++++++++++++++++++++++ 8 files changed, 947 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 pytest.ini create mode 100644 tests/conftest.py create mode 100644 tests/requirements.txt create mode 100644 tests/system/test_pipeline.py create mode 100644 tests/unit/test_crates_transformer.py create mode 100644 tests/unit/test_db_models.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2abbd70 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: ["main"] + paths: + - "**/*.py" + - "tests/**" + - "core/**" + - "package_managers/**" + pull_request: + branches: ["main"] + paths: + - "**/*.py" + - "tests/**" + - "core/**" + - "package_managers/**" + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/requirements.txt + - name: Run tests + run: | + pytest tests/unit/test_crates_transformer.py -v -m transformer --cov=core --cov-report=xml --cov-report=term-missing + pytest tests/unit/test_db_models.py -v -m db --cov=core --cov-append --cov-report=xml --cov-report=term-missing + pytest tests/system -v -m system --cov=core --cov-append --cov-report=xml --cov-report=term-missing diff --git a/core/models/__init__.py b/core/models/__init__.py index bc43885..1ef17e4 100644 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -12,7 +12,7 @@ func, ) from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Mapped, declarative_base, relationship naming_convention = { "ix": "ix_%(column_0_label)s", @@ -117,8 +117,8 @@ class Version(Base): DateTime, nullable=False, default=func.now(), server_default=func.now() ) - # package: Mapped["Package"] = relationship() - # license: Mapped["License"] = relationship() + package: Mapped["Package"] = relationship() + license: Mapped["License"] = relationship() def to_dict(self): return { @@ -184,6 +184,10 @@ class DependsOn(Base): DateTime, nullable=False, default=func.now(), server_default=func.now() ) + version: Mapped["Version"] = relationship() + dependency: Mapped["Package"] = relationship() + dependency_type: Mapped["DependsOnType"] = relationship() + def to_dict(self): return { "version_id": self.version_id, diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5bc3bd4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,15 @@ +[pytest] +# Test discovery paths +testpaths = tests + +# Python paths for test discovery +pythonpath = . + +# Markers for different test types +markers = + transformer: Unit tests for transformer classes + db: Unit tests for database models and operations + system: End-to-end system tests requiring full setup + +# Configure test paths +addopts = --import-mode=importlib \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..50dd20c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,146 @@ +""" +Common test fixtures and configurations. +""" + +import uuid +from unittest.mock import MagicMock + +import pytest +import testing.postgresql +from sqlalchemy import create_engine, event, text +from sqlalchemy.orm import Session + +from core.config import URLTypes, UserTypes +from core.db import DB +from core.models import Base, PackageManager, Source, URLType + + +@pytest.fixture(scope="session") +def mock_db(): + """ + Create a mock DB with necessary methods for transformer tests. + This fixture provides consistent mock objects for URL types and sources. + """ + db = MagicMock(spec=DB) + + # Mock URL types with consistent UUIDs + homepage_type = MagicMock() + homepage_type.id = uuid.UUID("00000000-0000-0000-0000-000000000001") + repository_type = MagicMock() + repository_type.id = uuid.UUID("00000000-0000-0000-0000-000000000002") + documentation_type = MagicMock() + documentation_type.id = uuid.UUID("00000000-0000-0000-0000-000000000003") + source_type = MagicMock() + source_type.id = uuid.UUID("00000000-0000-0000-0000-000000000004") + + db.select_url_types_homepage.return_value = homepage_type + db.select_url_types_repository.return_value = repository_type + db.select_url_types_documentation.return_value = documentation_type + db.select_url_types_source.return_value = source_type + + # Mock sources with consistent UUIDs + github_source = MagicMock() + github_source.id = uuid.UUID("00000000-0000-0000-0000-000000000005") + crates_source = MagicMock() + crates_source.id = uuid.UUID("00000000-0000-0000-0000-000000000006") + + db.select_source_by_name.side_effect = lambda name: { + "github": github_source, + "crates": crates_source, + }[name] + + return db + + +@pytest.fixture(scope="session") +def url_types(mock_db): + """Provide URL types configuration for tests.""" + return URLTypes(mock_db) + + +@pytest.fixture(scope="session") +def user_types(mock_db): + """Provide user types configuration for tests.""" + return UserTypes(mock_db) + + +@pytest.fixture(scope="class") +def pg_db(): + """ + Create a temporary PostgreSQL database for integration tests. + This database is recreated for each test class. + """ + with testing.postgresql.Postgresql() as postgresql: + yield postgresql + + +@pytest.fixture +def db_session(pg_db): + """ + Create a database session using temporary PostgreSQL. + This fixture handles database initialization and cleanup. + """ + engine = create_engine(pg_db.url()) + + # Create UUID extension for PostgreSQL + @event.listens_for(Base.metadata, "before_create") + def create_uuid_function(target, connection, **kw): + connection.execute( + text(""" + CREATE OR REPLACE FUNCTION uuid_generate_v4() + RETURNS uuid + AS $$ + BEGIN + RETURN gen_random_uuid(); + END; + $$ LANGUAGE plpgsql; + """) + ) + + Base.metadata.create_all(engine) + + with Session(engine) as session: + # Initialize URL types + for url_type_name in ["homepage", "repository", "documentation", "source"]: + existing_url_type = ( + session.query(URLType).filter_by(name=url_type_name).first() + ) + if not existing_url_type: + session.add(URLType(name=url_type_name)) + session.commit() + + # Initialize sources + for source_type in ["github", "crates"]: + existing_source = session.query(Source).filter_by(type=source_type).first() + if not existing_source: + session.add(Source(type=source_type)) + session.commit() + + # Initialize package manager + crates_source = session.query(Source).filter_by(type="crates").first() + existing_package_manager = ( + session.query(PackageManager).filter_by(source_id=crates_source.id).first() + ) + if not existing_package_manager: + package_manager = PackageManager(source_id=crates_source.id) + session.add(package_manager) + session.commit() + + yield session + session.rollback() + + +@pytest.fixture +def mock_csv_reader(): + """ + Fixture to mock CSV reading functionality. + Provides a consistent way to mock _read_csv_rows across transformer tests. + """ + + def create_mock_reader(data): + def mock_reader(file_key): + return [data].__iter__() + + return mock_reader + + return create_mock_reader diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..d173111 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,5 @@ +pytest==8.1.1 +testing.postgresql==1.3.0 +sqlalchemy==2.0.28 +psycopg2-binary==2.9.9 +pytest-cov==4.1.0 \ No newline at end of file diff --git a/tests/system/test_pipeline.py b/tests/system/test_pipeline.py new file mode 100644 index 0000000..b5db875 --- /dev/null +++ b/tests/system/test_pipeline.py @@ -0,0 +1,85 @@ +""" +System tests for the complete data pipeline. + +These tests verify the entire system working together with: +1. Real PostgreSQL database +2. Actual data transformations +3. End-to-end data flow + +These tests require the full Docker Compose setup and are skipped +if the required environment is not available. +""" + +import os + +import pytest +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session + +from core.models import Base + + +@pytest.mark.system +class TestSystemIntegration: + """ + System tests that require the full Docker Compose setup. + These tests verify the entire system working together. + """ + + def is_postgres_ready(self): + """ + Check if PostgreSQL is available. + + Returns: + bool: True if PostgreSQL is accessible, False otherwise + """ + try: + engine = create_engine( + os.environ.get( + "CHAI_DATABASE_URL", + "postgresql://postgres:s3cr3t@localhost:5435/chai", + ) + ) + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + return True + except Exception as e: + print(f"PostgreSQL not ready: {e}") + return False + + @pytest.fixture + def db_session(self): + """ + Create a PostgreSQL database session. + + This fixture: + 1. Checks if PostgreSQL is available + 2. Creates all tables if they don't exist + 3. Provides a session for the test + 4. Rolls back changes after the test + """ + if not self.is_postgres_ready(): + pytest.skip("PostgreSQL is not available") + + engine = create_engine(os.environ.get("CHAI_DATABASE_URL")) + Base.metadata.create_all(engine) + + with Session(engine) as session: + yield session + session.rollback() + + @pytest.mark.skipif( + not os.environ.get("RUN_SYSTEM_TESTS"), reason="System tests not enabled" + ) + def test_full_pipeline(self, db_session): + """ + Test the entire pipeline with actual database. + + This test verifies: + 1. Data loading from CSV files + 2. Transformation of raw data + 3. Database schema compatibility + 4. Data integrity across models + """ + # TODO: Implement full pipeline test + pass diff --git a/tests/unit/test_crates_transformer.py b/tests/unit/test_crates_transformer.py new file mode 100644 index 0000000..7844f46 --- /dev/null +++ b/tests/unit/test_crates_transformer.py @@ -0,0 +1,250 @@ +""" +Unit tests for the CratesTransformer class. + +These tests verify the transformation logic from raw data to database models: +1. Package data transformation +2. Version data transformation +3. Dependency relationship transformation +4. User data transformation +5. URL data transformation + +Each test uses a mock CSV reader to simulate data input and verifies +the correct transformation of that data into the expected format. + +The test data comes from a real row from the crates.io database dump. +""" + +import pytest + +from package_managers.crates.structs import DependencyType +from package_managers.crates.transformer import CratesTransformer + + +@pytest.mark.transformer +class TestTransformer: + """Tests for the CratesTransformer class""" + + @pytest.fixture + def transformer(self, url_types, user_types): + """Create a transformer instance with mocked dependencies""" + return CratesTransformer(url_types=url_types, user_types=user_types) + + def test_packages_transform(self, transformer, mock_csv_reader): + """ + Test package data transformation. + + Verifies: + - Basic field mapping (id -> import_id, etc.) + - Handling of optional fields (readme) + - Data type conversions + """ + test_data = { + "id": "123", + "name": "serde", + "readme": "# Serde\nA serialization framework", + } + + transformer._read_csv_rows = mock_csv_reader(test_data) + + packages = list(transformer.packages()) + assert len(packages) == 1 + + package = packages[0] + assert package["name"] == "serde" + assert package["import_id"] == "123" + assert package["readme"] == "# Serde\nA serialization framework" + + def test_versions_transform(self, transformer, mock_csv_reader): + """ + Test version data transformation. + + Verifies: + - Field mapping from crate fields to version fields + - Type conversions (size to int, etc.) + - Handling of timestamps + - Processing of optional fields + """ + test_data = { + "crate_id": "123", + "num": "1.0.0", + "id": "456", + "crate_size": "1000", + "created_at": "2023-01-01T00:00:00Z", + "license": "MIT", + "downloads": "5000", + "checksum": "abc123", + } + + transformer._read_csv_rows = mock_csv_reader(test_data) + + versions = list(transformer.versions()) + assert len(versions) == 1 + + version = versions[0] + assert version["crate_id"] == "123" + assert version["version"] == "1.0.0" + assert version["import_id"] == "456" + assert version["size"] == 1000 + assert version["published_at"] == "2023-01-01T00:00:00Z" + assert version["license"] == "MIT" + assert version["downloads"] == 5000 + assert version["checksum"] == "abc123" + + def test_dependencies_transform(self, transformer, mock_csv_reader): + """ + Test dependency data transformation. + + Verifies: + - Correct mapping of dependency fields + - Handling of dependency types + - Processing of version requirements + """ + test_data = { + "version_id": "456", + "crate_id": "789", + "req": "^1.0", + "kind": "0", # normal dependency + } + + transformer._read_csv_rows = mock_csv_reader(test_data) + + dependencies = list(transformer.dependencies()) + assert len(dependencies) == 1 + + dependency = dependencies[0] + assert dependency["version_id"] == "456" + assert dependency["crate_id"] == "789" + assert dependency["semver_range"] == "^1.0" + assert dependency["dependency_type"] == DependencyType(0) + + def test_users_transform(self, transformer, mock_csv_reader): + """ + Test user data transformation. + + Verifies: + - Mapping of GitHub login to username + - Correct source assignment + - Import ID handling + """ + test_data = {"gh_login": "alice", "id": "user123"} + + transformer._read_csv_rows = mock_csv_reader(test_data) + + users = list(transformer.users()) + assert len(users) == 1 + + user = users[0] + assert user["import_id"] == "user123" + assert user["username"] == "alice" + assert user["source_id"] == transformer.user_types.github + + def test_package_urls_transform(self, transformer, mock_csv_reader): + """ + Test package URLs transformation. + + Verifies: + - Creation of separate URL entries for each type + - Correct URL type assignment + - Handling of missing URLs + """ + test_data = { + "id": "123", + "homepage": "https://serde.rs", + "repository": "https://github.com/serde-rs/serde", + "documentation": "https://docs.rs/serde", + } + + transformer._read_csv_rows = mock_csv_reader(test_data) + + urls = list(transformer.package_urls()) + assert len(urls) == 3 # One for each URL type + + # Check homepage URL + homepage = next( + url for url in urls if url["url_type_id"] == transformer.url_types.homepage + ) + assert homepage["import_id"] == "123" + assert homepage["url"] == "https://serde.rs" + + # Check repository URL + repo = next( + url + for url in urls + if url["url_type_id"] == transformer.url_types.repository + ) + assert repo["import_id"] == "123" + assert repo["url"] == "https://github.com/serde-rs/serde" + + # Check documentation URL + docs = next( + url + for url in urls + if url["url_type_id"] == transformer.url_types.documentation + ) + assert docs["import_id"] == "123" + assert docs["url"] == "https://docs.rs/serde" + + def test_user_versions_transform(self, transformer, mock_csv_reader): + """ + Test user versions data transformation. + + Verifies: + - Mapping of version publishing data + - Correct handling of user associations + """ + test_data = {"id": "version123", "published_by": "user456"} + + transformer._read_csv_rows = mock_csv_reader(test_data) + + user_versions = list(transformer.user_versions()) + assert len(user_versions) == 1 + + user_version = user_versions[0] + assert user_version["version_id"] == "version123" + assert user_version["published_by"] == "user456" + + def test_urls_transform(self, transformer, mock_csv_reader): + """ + Test URLs transformation. + + Verifies: + - Extraction of URLs from package data + - Correct type assignment for each URL + - Handling of all URL types + """ + test_data = { + "homepage": "https://serde.rs", + "repository": "https://github.com/serde-rs/serde", + "documentation": "https://docs.rs/serde", + } + + transformer._read_csv_rows = mock_csv_reader(test_data) + + urls = list(transformer.urls()) + assert len(urls) == 3 # One for each URL type + + # Check that each URL type is present + url_types_found = {url["url_type_id"] for url in urls} + assert transformer.url_types.homepage in url_types_found + assert transformer.url_types.repository in url_types_found + assert transformer.url_types.documentation in url_types_found + + # Check specific URLs + homepage = next( + url for url in urls if url["url_type_id"] == transformer.url_types.homepage + ) + assert homepage["url"] == "https://serde.rs" + + repo = next( + url + for url in urls + if url["url_type_id"] == transformer.url_types.repository + ) + assert repo["url"] == "https://github.com/serde-rs/serde" + + docs = next( + url + for url in urls + if url["url_type_id"] == transformer.url_types.documentation + ) + assert docs["url"] == "https://docs.rs/serde" diff --git a/tests/unit/test_db_models.py b/tests/unit/test_db_models.py new file mode 100644 index 0000000..ae487fc --- /dev/null +++ b/tests/unit/test_db_models.py @@ -0,0 +1,400 @@ +""" +Unit tests for database models and their relationships. + +These tests verify: +1. Basic CRUD operations for all models +2. Relationship integrity between models +3. Constraint enforcement (unique, foreign key, etc.) +4. Data type handling (UUIDs, timestamps, etc.) + +The tests use a temporary PostgreSQL database that is created and destroyed +for each test class, ensuring test isolation and cleanup. +""" + +import uuid + +import pytest + +from core.models import ( + DependsOn, + DependsOnType, + License, + Package, + PackageManager, + Version, +) + + +class TestDatabaseModels: + """ + Unit tests for database models and operations. + Uses a temporary PostgreSQL database to verify model behavior and relationships. + """ + + @pytest.mark.db + def test_package_crud(self, db_session): + """ + Test CRUD operations for Package model. + + Verifies: + - Package creation with required fields + - Reading package data + - Updating package fields + - Deleting a package + - Unique constraint on derived_id + - Foreign key constraint with package_manager + """ + # Get package manager for test + package_manager = db_session.query(PackageManager).first() + + # Create + package = Package( + id=uuid.uuid4(), + import_id="test123", + name="test-package", + readme="Test readme", + package_manager_id=package_manager.id, + derived_id=f"crates/test-package-{uuid.uuid4().hex[:8]}", + ) + db_session.add(package) + db_session.commit() + + # Read + saved_package = db_session.query(Package).filter_by(import_id="test123").first() + assert saved_package is not None + assert saved_package.name == "test-package" + + # Update + saved_package.readme = "Updated readme" + db_session.commit() + + # Delete + db_session.delete(saved_package) + db_session.commit() + assert db_session.query(Package).filter_by(import_id="test123").first() is None + + @pytest.mark.db + def test_version_relationships(self, db_session): + """ + Test relationships between Version and Package models. + + Verifies: + - Version creation with package relationship + - Foreign key constraint with package + - Unique constraint on package_id + version + - Bidirectional navigation between Version and Package + """ + # Get package manager for test + package_manager = db_session.query(PackageManager).first() + + # Create package with unique identifiers + import_id = f"pkg{uuid.uuid4().hex[:8]}" + derived_id = f"crates/test-package-{uuid.uuid4().hex[:8]}" + package = Package( + id=uuid.uuid4(), + import_id=import_id, + name="test-package", + package_manager_id=package_manager.id, + derived_id=derived_id, + ) + db_session.add(package) + db_session.commit() + + # Create version + version = Version( + id=uuid.uuid4(), + import_id=f"ver{uuid.uuid4().hex[:8]}", + package_id=package.id, + version="1.0.0", + ) + db_session.add(version) + db_session.commit() + + # Query relationships + saved_version = db_session.query(Version).filter_by(id=version.id).first() + assert saved_version is not None + assert saved_version.package_id == package.id + assert saved_version.package.name == "test-package" + + @pytest.mark.db + def test_license_crud(self, db_session): + """ + Test CRUD operations for License model. + + Verifies: + - License creation with required fields + - Reading license data + - Updating license fields + - Deleting a license + - Unique constraint on license name + """ + # Create + license = License( + id=uuid.uuid4(), + name="MIT", + ) + db_session.add(license) + db_session.commit() + + # Read + saved_license = db_session.query(License).filter_by(name="MIT").first() + assert saved_license is not None + assert saved_license.name == "MIT" + + # Test unique constraint + duplicate_license = License( + id=uuid.uuid4(), + name="MIT", # Same name as existing license + ) + db_session.add(duplicate_license) + with pytest.raises( + Exception + ) as exc_info: # SQLAlchemy will raise an integrity error + db_session.commit() + assert "duplicate key value violates unique constraint" in str(exc_info.value) + db_session.rollback() + + # Update + saved_license.name = "Apache-2.0" + db_session.commit() + + # Verify update + updated_license = db_session.get(License, saved_license.id) + assert updated_license.name == "Apache-2.0" + + # Delete + db_session.delete(saved_license) + db_session.commit() + assert db_session.query(License).filter_by(name="Apache-2.0").first() is None + + @pytest.mark.db + def test_license_version_relationship(self, db_session): + """ + Test relationships between License and Version models. + + Verifies: + - Version creation with license relationship + - Foreign key constraint with license + - Nullable license_id field + - Bidirectional navigation between Version and License + """ + # Get package manager for test + package_manager = db_session.query(PackageManager).first() + + # Create license + license = License( + id=uuid.uuid4(), + name="MIT", + ) + db_session.add(license) + db_session.commit() + + # Create package with unique identifiers + import_id = f"pkg{uuid.uuid4().hex[:8]}" + derived_id = f"crates/test-package-{uuid.uuid4().hex[:8]}" + package = Package( + id=uuid.uuid4(), + import_id=import_id, + name="test-package", + package_manager_id=package_manager.id, + derived_id=derived_id, + ) + db_session.add(package) + db_session.commit() + + # Create version with license + version = Version( + id=uuid.uuid4(), + package_id=package.id, + version="1.0.0", + license_id=license.id, + import_id=f"ver{uuid.uuid4().hex[:8]}", + ) + db_session.add(version) + db_session.commit() + + # Test relationships + saved_version = db_session.query(Version).filter_by(id=version.id).first() + assert saved_version.license_id == license.id + assert saved_version.license.name == "MIT" + + @pytest.mark.db + def test_depends_on_crud(self, db_session): + """ + Test CRUD operations for DependsOn model. + + Verifies: + - Dependency creation with required fields + - Reading dependency data + - Updating dependency fields + - Deleting a dependency + - Foreign key constraints with version and package + - Unique constraint on version + dependency + type + """ + # Get package manager for test + package_manager = db_session.query(PackageManager).first() + + # Create dependency type + dep_type = DependsOnType( + id=uuid.uuid4(), + name="runtime", + ) + db_session.add(dep_type) + db_session.commit() + + # Create two packages with unique identifiers + package1 = Package( + id=uuid.uuid4(), + import_id=f"pkg{uuid.uuid4().hex[:8]}", + name="package-one", + package_manager_id=package_manager.id, + derived_id=f"crates/package-one-{uuid.uuid4().hex[:8]}", + ) + package2 = Package( + id=uuid.uuid4(), + import_id=f"pkg{uuid.uuid4().hex[:8]}", + name="package-two", + package_manager_id=package_manager.id, + derived_id=f"crates/package-two-{uuid.uuid4().hex[:8]}", + ) + db_session.add_all([package1, package2]) + db_session.commit() + + # Create version for package1 + version = Version( + id=uuid.uuid4(), + import_id=f"ver{uuid.uuid4().hex[:8]}", + package_id=package1.id, + version="1.0.0", + ) + db_session.add(version) + db_session.commit() + + # Create dependency relationship + dependency = DependsOn( + id=uuid.uuid4(), + version_id=version.id, + dependency_id=package2.id, + dependency_type_id=dep_type.id, + semver_range="^1.0", + ) + db_session.add(dependency) + db_session.commit() + + # Read + saved_dep = ( + db_session.query(DependsOn) + .filter_by(version_id=version.id, dependency_id=package2.id) + .first() + ) + assert saved_dep is not None + assert saved_dep.semver_range == "^1.0" + assert saved_dep.dependency_type_id == dep_type.id + + # Update + saved_dep.semver_range = "^2.0" + db_session.commit() + + # Verify update + updated_dep = db_session.get(DependsOn, saved_dep.id) + assert updated_dep.semver_range == "^2.0" + + # Delete + db_session.delete(saved_dep) + db_session.commit() + assert ( + db_session.query(DependsOn) + .filter_by(version_id=version.id, dependency_id=package2.id) + .first() + is None + ) + + @pytest.mark.db + def test_depends_on_relationships(self, db_session): + """ + Test complex relationships between DependsOn and related models. + + Verifies: + - Relationships between DependsOn, Version, Package, and DependsOnType + - Correct navigation through multiple relationship levels + - Integrity of relationship data + - Proper handling of nullable fields (dependency_type) + """ + # Get package manager for test + package_manager = db_session.query(PackageManager).first() + + # Get or create dependency types + runtime_type = db_session.query(DependsOnType).filter_by(name="runtime").first() + if not runtime_type: + runtime_type = DependsOnType( + id=uuid.uuid4(), + name="runtime", + ) + db_session.add(runtime_type) + + dev_type = db_session.query(DependsOnType).filter_by(name="dev").first() + if not dev_type: + dev_type = DependsOnType( + id=uuid.uuid4(), + name="dev", + ) + db_session.add(dev_type) + db_session.commit() + + # Create packages with unique identifiers + import_id1 = f"pkg{uuid.uuid4().hex[:8]}" + import_id2 = f"pkg{uuid.uuid4().hex[:8]}" + derived_id1 = f"crates/test-package-1-{uuid.uuid4().hex[:8]}" + derived_id2 = f"crates/test-package-2-{uuid.uuid4().hex[:8]}" + package1 = Package( + id=uuid.uuid4(), + import_id=import_id1, + name="test-package-1", + package_manager_id=package_manager.id, + derived_id=derived_id1, + ) + package2 = Package( + id=uuid.uuid4(), + import_id=import_id2, + name="test-package-2", + package_manager_id=package_manager.id, + derived_id=derived_id2, + ) + db_session.add_all([package1, package2]) + db_session.commit() + + # Create versions with import_ids + version1 = Version( + id=uuid.uuid4(), + package_id=package1.id, + version="1.0.0", + import_id=f"ver{uuid.uuid4().hex[:8]}", + ) + version2 = Version( + id=uuid.uuid4(), + package_id=package2.id, + version="2.0.0", + import_id=f"ver{uuid.uuid4().hex[:8]}", + ) + db_session.add_all([version1, version2]) + db_session.commit() + + # Create dependency relationship + depends_on = DependsOn( + id=uuid.uuid4(), + version_id=version1.id, + dependency_id=package2.id, + dependency_type_id=runtime_type.id, + semver_range="^2.0", + ) + db_session.add(depends_on) + db_session.commit() + + # Test relationships + saved_dep = db_session.query(DependsOn).filter_by(id=depends_on.id).first() + assert saved_dep.version_id == version1.id + assert saved_dep.dependency_id == package2.id + assert saved_dep.dependency_type_id == runtime_type.id + assert saved_dep.version.package.name == "test-package-1" + assert saved_dep.dependency.name == "test-package-2" + assert saved_dep.dependency_type.name == "runtime"