"""Integration tests for repository API endpoints. These tests access app components directly outside the API to set up specific conditions (e.g., corrupted metadata) that can't be created through normal API usage. Unlike pure API tests in test_shed_repositories.py, these tests verify the API correctly handles edge cases that require backend manipulation to reproduce. """ import copy from dataclasses import dataclass from sqlalchemy import select from galaxy_test.base import api_asserts from tool_shed.test.base import test_db_util from tool_shed.webapp import model from ..base.api import ShedApiTestCase CORRUPTED_PATH = "/old/wrong/path/column_maker.xml" @dataclass class CorruptedRepoFixture: """Tracks state for a repository with corrupted metadata.""" repository: object # API repository object db_repo: model.Repository metadata_revision_id: int corrupted_path: str def corrupt_tool_config_path(repository, corrupted_path: str = CORRUPTED_PATH) -> CorruptedRepoFixture: """Corrupt tool_config path in repository metadata via direct DB access.""" sa_session = test_db_util.sa_session() db_repo = test_db_util.get_repository_by_name_and_owner(repository.name, repository.owner) assert db_repo is not None assert len(db_repo.metadata_revisions) >= 1 # Find a revision with tools metadata_revision = None for rev in db_repo.metadata_revisions: if rev.metadata and rev.metadata.get("tools"): metadata_revision = rev break assert metadata_revision is not None, "Expected repository to have tools" # Corrupt the metadata (must create new dict for MutableJSONType) new_metadata = copy.deepcopy(metadata_revision.metadata) new_metadata["tools"][0]["tool_config"] = corrupted_path metadata_revision.metadata = new_metadata sa_session.add(metadata_revision) sa_session.commit() # Verify corruption persisted sa_session.expire_all() metadata_revision = sa_session.get(model.RepositoryMetadata, metadata_revision.id) assert metadata_revision.metadata["tools"][0]["tool_config"] == corrupted_path return CorruptedRepoFixture( repository=repository, db_repo=db_repo, metadata_revision_id=metadata_revision.id, corrupted_path=corrupted_path, ) def metadata_contains_path(metadata_dict: dict, path: str) -> bool: """Check if any tool in metadata dict has the given tool_config path.""" for rev_data in metadata_dict.values(): for tool in rev_data.get("tools") or []: if tool.get("tool_config") == path: return True return False def metadata_has_valid_paths(metadata_dict: dict, invalid_prefix: str = "/old/wrong") -> bool: """Check if metadata has at least one valid (non-corrupted) tool_config path.""" for rev_data in metadata_dict.values(): for tool in rev_data.get("tools") or []: tool_config = tool.get("tool_config", "") if tool_config and not tool_config.startswith(invalid_prefix): return True return False class TestRepositoriesIntegration(ShedApiTestCase): def test_reset_metadata_dry_run_shows_corrupted_path_fix(self): """Verify dry_run=True shows before/after diff for corrupted tool_config paths. This simulates a scenario where repository metadata has stale absolute paths (e.g., after file_path config change). The dry_run preview should show: - repository_metadata_before: contains corrupted path - repository_metadata_after: contains fixed path - DB remains unchanged (dry_run doesn't persist) """ repository = self.populator.setup_column_maker_repo(prefix="integcorrupt") fixture = corrupt_tool_config_path(repository) # Call reset_metadata API with dry_run=True, verbose=True response = self.api_interactor.post( f"repositories/{repository.id}/reset_metadata", params={"dry_run": True, "verbose": True}, ) api_asserts.assert_status_code_is_ok(response) result = response.json() assert result["dry_run"] is True assert result["status"] == "ok" assert result["repository_metadata_before"] is not None assert result["repository_metadata_after"] is not None before = result["repository_metadata_before"] after = result["repository_metadata_after"] assert metadata_contains_path( before, fixture.corrupted_path ), f"Expected before snapshot to contain corrupted path '{fixture.corrupted_path}'" assert not metadata_contains_path( after, fixture.corrupted_path ), "After snapshot should not contain corrupted path" assert metadata_has_valid_paths(after), "Expected after snapshot to contain fixed path" # Verify DB still has corrupted data (dry_run shouldn't persist) sa_session = test_db_util.sa_session() sa_session.expire_all() metadata_revision = sa_session.get(model.RepositoryMetadata, fixture.metadata_revision_id) assert ( metadata_revision.metadata["tools"][0]["tool_config"] == fixture.corrupted_path ), "dry_run should not have modified the database" def test_reset_metadata_fixes_corrupted_path_when_not_dry_run(self): """Verify non-dry-run reset actually fixes corrupted tool_config paths.""" repository = self.populator.setup_column_maker_repo(prefix="integfix") fixture = corrupt_tool_config_path(repository) # Call reset_metadata without dry_run response = self.api_interactor.post( f"repositories/{repository.id}/reset_metadata", params={"dry_run": False, "verbose": True}, ) api_asserts.assert_status_code_is_ok(response) result = response.json() assert result["dry_run"] is False assert result["status"] == "ok" # Verify DB now has fixed path sa_session = test_db_util.sa_session() sa_session.expire_all() stmt = select(model.RepositoryMetadata).where(model.RepositoryMetadata.repository_id == fixture.db_repo.id) revisions = sa_session.scalars(stmt).all() assert len(revisions) >= 1 # Build metadata dict from DB revisions for reuse of helper db_metadata = {str(rev.id): rev.metadata for rev in revisions if rev.metadata} assert metadata_has_valid_paths(db_metadata), "Expected reset to fix the corrupted tool_config path"