import unittest from unittest.mock import AsyncMock, MagicMock, patch import sys from pathlib import Path import httpx # Add opax-mcp to path to allow direct import of 'server' REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "opax-mcp")) # Stub unrelated dependencies before importing server, matching existing tests sys.modules['emma_adapter'] = MagicMock() # Now that the path and stubs are set, import the modules under test from server import fetch_and_normalize_source from deployment_source import SourceArtifactError class TestFetchAndNormalizeSource(unittest.IsolatedAsyncioTestCase): def setUp(self): self.repo_id = "owner/repo" self.required_paths = ("cloudbuild.deploy.yaml",) self.resolved_sha = "b" * 40 self.mock_policy = { "repository": self.repo_id, "required_source_paths": self.required_paths, } self.mock_manifest = { "sha256": "normalized_sha256_hash", "source_bytes": 54321, "wrapper_directory_stripped": True, } # Patch GITEA_URL as it's used directly by the handler self.gitea_url_patch = patch('server.GITEA_URL', 'https://gitea.test') self.gitea_url_patch.start() def tearDown(self): self.gitea_url_patch.stop() @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock) @patch('server.get_deployment_target') async def test_branch_ref_success(self, m_get_target, m_resolve_sha, m_download, m_normalize): m_get_target.return_value = self.mock_policy m_resolve_sha.return_value = self.resolved_sha m_download.return_value = b"archive data" m_normalize.return_value = (b"normalized data", self.mock_manifest) result = await fetch_and_normalize_source({"service_key": "test-svc", "ref": "main"}) m_get_target.assert_called_once_with("test-svc") m_resolve_sha.assert_called_once_with(branch_name="main", repo_id=self.repo_id, gitea_url='https://gitea.test') m_download.assert_awaited_once_with(commit_sha=self.resolved_sha, repo_id=self.repo_id, gitea_url='https://gitea.test') m_normalize.assert_called_once_with(archive_bytes=b"archive data", required_paths=self.required_paths) self.assertEqual(result["resolved_commit_sha"], self.resolved_sha) self.assertEqual(result["requested_ref"], "main") self.assertEqual(result["sha256"], self.mock_manifest["sha256"]) self.assertEqual(set(result.keys()), { "service_key", "repository", "requested_ref", "resolved_commit_sha", "sha256", "source_bytes", "wrapper_directory_stripped" }) @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock) @patch('server.get_deployment_target') async def test_full_sha_success(self, m_get_target, m_resolve_sha, m_download, m_normalize): m_get_target.return_value = self.mock_policy m_download.return_value = b"archive data" m_normalize.return_value = (b"normalized data", self.mock_manifest) uppercase_sha = self.resolved_sha.upper() result = await fetch_and_normalize_source({"service_key": "test-svc", "ref": uppercase_sha}) m_resolve_sha.assert_not_called() m_download.assert_awaited_once_with(commit_sha=self.resolved_sha, repo_id=self.repo_id, gitea_url='https://gitea.test') self.assertEqual(result["resolved_commit_sha"], self.resolved_sha) self.assertEqual(result["requested_ref"], uppercase_sha) @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock) @patch('server.get_deployment_target') async def test_invalid_service_key_fails_before_dependencies(self, m_get_target, m_resolve_sha, m_download, m_normalize): for invalid_key in ["", None, 123]: with self.subTest(key=invalid_key): with self.assertRaisesRegex(ValueError, "Missing or invalid 'service_key'"): await fetch_and_normalize_source({"service_key": invalid_key, "ref": "main"}) m_get_target.assert_not_called() m_resolve_sha.assert_not_called() m_download.assert_not_called() m_normalize.assert_not_called() @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock) @patch('server.get_deployment_target') async def test_invalid_ref_fails_before_dependencies(self, m_get_target, m_resolve_sha, m_download, m_normalize): for invalid_ref in ["", None, 123]: with self.subTest(ref=invalid_ref): with self.assertRaisesRegex(ValueError, "Missing or invalid 'ref'"): await fetch_and_normalize_source({"service_key": "test-svc", "ref": invalid_ref}) m_get_target.assert_not_called() m_resolve_sha.assert_not_called() m_download.assert_not_called() m_normalize.assert_not_called() @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock) @patch('server.get_deployment_target', side_effect=ValueError("Unknown service")) async def test_unknown_service_propagates_before_gitea_calls(self, m_get_target, m_resolve_sha, m_download, m_normalize): with self.assertRaisesRegex(ValueError, "Unknown service"): await fetch_and_normalize_source({"service_key": "unknown", "ref": "main"}) m_resolve_sha.assert_not_called() m_download.assert_not_called() m_normalize.assert_not_called() @patch('server.normalize_gitea_archive') @patch('server.download_repo_archive', new_callable=AsyncMock, side_effect=httpx.ReadTimeout("Timeout")) @patch('server.get_deployment_target') async def test_archive_download_error_propagates(self, m_get_target, m_download, m_normalize): m_get_target.return_value = self.mock_policy with self.assertRaises(httpx.ReadTimeout): await fetch_and_normalize_source({"service_key": "test-svc", "ref": self.resolved_sha}) m_download.assert_awaited_once() m_normalize.assert_not_called() @patch('server.normalize_gitea_archive', side_effect=SourceArtifactError("Normalization failed")) @patch('server.download_repo_archive', new_callable=AsyncMock) @patch('server.get_deployment_target') async def test_normalization_error_propagates(self, m_get_target, m_download, m_normalize): m_get_target.return_value = self.mock_policy m_download.return_value = b"archive data" with self.assertRaisesRegex(SourceArtifactError, "Normalization failed"): await fetch_and_normalize_source({"service_key": "test-svc", "ref": self.resolved_sha}) m_download.assert_awaited_once() m_normalize.assert_called_once_with( archive_bytes=b"archive data", required_paths=self.required_paths, ) if __name__ == '__main__': unittest.main()