import unittest from pathlib import Path import sys from unittest.mock import AsyncMock, MagicMock, patch import os import httpx REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "opax-mcp")) from gitea_handler import handle_list_repo_files class TestGiteaHandler(unittest.TestCase): def setUp(self): self.original_gitea_url = os.environ.get("GITEA_URL") self.original_gitea_token = os.environ.get("GITEA_TOKEN") os.environ["GITEA_URL"] = "https://gitea.example.com" os.environ["GITEA_TOKEN"] = "test-token" def tearDown(self): if self.original_gitea_url is not None: os.environ["GITEA_URL"] = self.original_gitea_url else: del os.environ["GITEA_URL"] if self.original_gitea_token is not None: os.environ["GITEA_TOKEN"] = self.original_gitea_token else: del os.environ["GITEA_TOKEN"] def _prepare_mock_client(self, payload, is_file=False): response = MagicMock() response.raise_for_status = MagicMock() if is_file: response.json.return_value = {"name": "file.txt", "path": "file.txt", "type": "file"} else: response.json.return_value = payload client = MagicMock() client.get = AsyncMock(return_value=response) async_client = MagicMock() async_client.__aenter__ = AsyncMock(return_value=client) async_client.__aexit__ = AsyncMock(return_value=None) return async_client, client @patch("httpx.AsyncClient") def test_list_repo_files_root(self, mock_async_client_constructor): payload = [ {"name": "README.md", "path": "README.md", "type": "file", "sha": "sha123", "size": 1024}, {"name": "docs", "path": "docs", "type": "dir"} ] mock_async_client, _ = self._prepare_mock_client(payload) mock_async_client_constructor.return_value = mock_async_client async def run_test(): result = await handle_list_repo_files({"path": None}, "test/repo") self.assertEqual(result["path"], "") self.assertEqual(len(result["files"]), 2) self.assertEqual(result["files"][0]["name"], "README.md") self.assertIn("sha", result["files"][0]) self.assertIn("size", result["files"][0]) self.assertEqual(result["files"][1]["name"], "docs") self.assertNotIn("sha", result["files"][1]) self.assertNotIn("size", result["files"][1]) import asyncio asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_list_repo_files_empty_path(self, mock_async_client_constructor): payload = [] mock_async_client, _ = self._prepare_mock_client(payload) mock_async_client_constructor.return_value = mock_async_client async def run_test(): result = await handle_list_repo_files({"path": ""}, "test/repo") self.assertEqual(result["path"], "") import asyncio asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_list_repo_files_dot_path(self, mock_async_client_constructor): payload = [] mock_async_client, _ = self._prepare_mock_client(payload) mock_async_client_constructor.return_value = mock_async_client async def run_test(): result = await handle_list_repo_files({"path": "."}, "test/repo") self.assertEqual(result["path"], "") import asyncio asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_nested_path(self, mock_async_client_constructor): payload = [] mock_async_client, mock_client = self._prepare_mock_client(payload) mock_async_client_constructor.return_value = mock_async_client async def run_test(): await handle_list_repo_files({"path": "docs/security"}, "test/repo") mock_client.get.assert_called_once() called_url = mock_client.get.call_args[0][0] self.assertIn("docs/security", called_url) import asyncio asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_path_is_file(self, mock_async_client_constructor): mock_async_client, _ = self._prepare_mock_client(None, is_file=True) mock_async_client_constructor.return_value = mock_async_client async def run_test(): with self.assertRaisesRegex(ValueError, "Path is a file, not a directory. Use get_file_content instead."): await handle_list_repo_files({"path": "file.txt"}, "test/repo") import asyncio asyncio.run(run_test()) def test_invalid_paths(self): invalid_paths = [ "../secrets", "docs/../secrets", "docs\\secrets", "/etc/passwd", "https://example.com", "docs?ref=other", "docs#fragment", "docs//security", "docs/./security", ] async def run_test(): for path in invalid_paths: with self.subTest(path=path): with self.assertRaisesRegex(ValueError, "Invalid repository path."): await handle_list_repo_files({"path": path}, "test/repo") import asyncio asyncio.run(run_test()) def test_invalid_input_types(self): async def run_test(): with self.assertRaisesRegex(ValueError, "Invalid repository path."): await handle_list_repo_files({"path": 123}, "test/repo") import asyncio asyncio.run(run_test()) def test_unsupported_fields(self): unsupported = [ {"repo": "x"}, {"ref": "y"}, {"branch": "z"}, {"extra": "key"}, ] async def run_test(): for p in unsupported: with self.subTest(p=p): with self.assertRaisesRegex(ValueError, "Unsupported list_repo_files input field."): await handle_list_repo_files(p, "test/repo") import asyncio asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_uses_trusted_repo(self, mock_async_client_constructor): payload = [] mock_async_client, mock_client = self._prepare_mock_client(payload) mock_async_client_constructor.return_value = mock_async_client async def run_test(): await handle_list_repo_files({"path": "docs", "repo": "user/bad-repo"}, "test/repo") # The handler should raise an error due to the unsupported 'repo' field. import asyncio with self.assertRaisesRegex(ValueError, "Unsupported list_repo_files input field."): asyncio.run(run_test()) @patch("httpx.AsyncClient") def test_http_error_propagation(self, mock_async_client_constructor): response = MagicMock() response.raise_for_status.side_effect = httpx.HTTPStatusError("Error", request=MagicMock(), response=MagicMock()) client = MagicMock() client.get = AsyncMock(return_value=response) async_client = MagicMock() async_client.__aenter__ = AsyncMock(return_value=client) async_client.__aexit__ = AsyncMock(return_value=None) mock_async_client_constructor.return_value = async_client async def run_test(): with self.assertRaises(httpx.HTTPStatusError): await handle_list_repo_files({"path": ""}, "test/repo") import asyncio asyncio.run(run_test()) if __name__ == '__main__': unittest.main()