#!/usr/bin/env python3 """Tests for the GitLab CLI MCP server implementation.""" import json from unittest.mock import MagicMock, patch from mcp_server_gitlab_glab.server import GitLabServer class TestGitLabServer: """Tests for the GitLabServer class.""" def test_init(self) -> None: """Test initialization of GitLabServer.""" server = GitLabServer() assert hasattr(server, "auth_message") assert "glab auth login" in server.auth_message @patch("subprocess.run") def test_execute_glab_command_success(self, mock_run: MagicMock) -> None: """Test successful execution of a glab command.""" # Mock successful command execution mock_process = MagicMock() mock_process.returncode = 0 mock_process.stdout = "command output" mock_process.stderr = "" mock_run.return_value = mock_process server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["--version"], working_dir) assert success is True assert result == "command output" mock_run.assert_called_once_with( ["glab", "--version"], capture_output=True, text=True, check=False, cwd=working_dir, ) @patch("subprocess.run") def test_execute_glab_command_failure(self, mock_run: MagicMock) -> None: """Test failed execution of a glab command.""" # Mock failed command execution mock_process = MagicMock() mock_process.returncode = 1 mock_process.stdout = "" mock_process.stderr = "command failed" mock_run.return_value = mock_process server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["--version"], working_dir) assert success is False assert result == {"error": "command failed"} mock_run.assert_called_once_with( ["glab", "--version"], capture_output=True, text=True, check=False, cwd=working_dir, ) @patch("subprocess.run") def test_execute_glab_command_auth_error(self, mock_run: MagicMock) -> None: """Test authentication error during glab command execution.""" # Mock authentication error mock_process = MagicMock() mock_process.returncode = 1 mock_process.stdout = "" mock_process.stderr = "authentication required" mock_run.return_value = mock_process server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["api", "/projects"], working_dir) assert success is False assert "error" in result assert "auth login" in result["error"] mock_run.assert_called_once_with( ["glab", "api", "/projects"], capture_output=True, text=True, check=False, cwd=working_dir, ) @patch("subprocess.run") def test_execute_glab_command_not_found(self, mock_run: MagicMock) -> None: """Test glab command not found error.""" # Mock FileNotFoundError mock_run.side_effect = FileNotFoundError("No such file or directory: 'glab'") server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["--version"], working_dir) assert success is False assert "error" in result assert "glab command not found" in result["error"] @patch("subprocess.run") def test_execute_glab_api_command_success(self, mock_run: MagicMock) -> None: """Test successful execution of a glab api command with JSON response.""" # Mock successful API command execution with JSON response mock_process = MagicMock() mock_process.returncode = 0 mock_process.stdout = json.dumps([{"id": 1, "name": "test-project"}]) mock_process.stderr = "" mock_run.return_value = mock_process server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["api", "/projects"], working_dir) assert success is True assert isinstance(result, list) assert len(result) == 1 assert result[0]["id"] == 1 assert result[0]["name"] == "test-project" mock_run.assert_called_once_with( ["glab", "api", "/projects"], capture_output=True, text=True, check=False, cwd=working_dir, ) @patch("subprocess.run") def test_execute_glab_api_command_invalid_json(self, mock_run: MagicMock) -> None: """Test glab api command with invalid JSON response.""" # Mock API command execution with invalid JSON response mock_process = MagicMock() mock_process.returncode = 0 mock_process.stdout = "invalid json" mock_process.stderr = "" mock_run.return_value = mock_process server = GitLabServer() working_dir = "/test/directory" success, result = server.execute_glab_command(["api", "/projects"], working_dir) assert success is False assert "error" in result assert "Failed to parse JSON response" in result["error"] @patch.object(GitLabServer, "execute_glab_command") def test_check_availability_success(self, mock_execute: MagicMock) -> None: """Test successful check_availability.""" # Mock successful command execution mock_execute.return_value = (True, "glab version 1.0.0") server = GitLabServer() working_dir = "/test/directory" result = server.check_availability(working_dir) assert result["available"] is True assert result["version"] == "glab version 1.0.0" mock_execute.assert_called_once_with(["--version"], working_dir) @patch.object(GitLabServer, "execute_glab_command") def test_check_availability_failure(self, mock_execute: MagicMock) -> None: """Test failed check_availability.""" # Mock failed command execution mock_execute.return_value = (False, {"error": "glab command not found"}) server = GitLabServer() working_dir = "/test/directory" result = server.check_availability(working_dir) assert result["available"] is False assert result["error"] == "glab command not found" mock_execute.assert_called_once_with(["--version"], working_dir) @patch.object(GitLabServer, "execute_glab_command") def test_find_project_success(self, mock_execute: MagicMock) -> None: """Test successful find_project with a single project.""" # Mock successful API response with a project mock_execute.return_value = ( True, [ { "id": 1, "name": "test-project", "path_with_namespace": "group/test-project", "web_url": "https://gitlab.com/group/test-project", "description": "A test project", } ], ) server = GitLabServer() working_dir = "/test/directory" result = server.find_project("test-project", working_dir) assert isinstance(result, list) assert len(result) == 1 assert result[0]["id"] == 1 assert result[0]["name"] == "test-project" assert result[0]["path_with_namespace"] == "group/test-project" assert result[0]["web_url"] == "https://gitlab.com/group/test-project" assert result[0]["description"] == "A test project" mock_execute.assert_called_once_with( ["api", "/projects?search_namespaces=true&search=test-project"], working_dir ) @patch.object(GitLabServer, "execute_glab_command") def test_find_project_multiple_results(self, mock_execute: MagicMock) -> None: """Test successful find_project with multiple projects.""" # Mock successful API response with multiple projects mock_execute.return_value = ( True, [ { "id": 1, "name": "test-project", "path_with_namespace": "group/test-project", "web_url": "https://gitlab.com/group/test-project", "description": "A test project", }, { "id": 2, "name": "test-project-2", "path_with_namespace": "group/test-project-2", "web_url": "https://gitlab.com/group/test-project-2", "description": "Another test project", }, { "id": 3, "name": "test-project-3", "path_with_namespace": "group/test-project-3", "web_url": "https://gitlab.com/group/test-project-3", "description": "Yet another test project", }, ], ) server = GitLabServer() working_dir = "/test/directory" result = server.find_project("test-project", working_dir) assert isinstance(result, list) assert len(result) == 3 # Check first project assert result[0]["id"] == 1 assert result[0]["name"] == "test-project" assert result[0]["path_with_namespace"] == "group/test-project" # Check second project assert result[1]["id"] == 2 assert result[1]["name"] == "test-project-2" assert result[1]["path_with_namespace"] == "group/test-project-2" # Check third project assert result[2]["id"] == 3 assert result[2]["name"] == "test-project-3" assert result[2]["path_with_namespace"] == "group/test-project-3" mock_execute.assert_called_once_with( ["api", "/projects?search_namespaces=true&search=test-project"], working_dir ) @patch.object(GitLabServer, "execute_glab_command") def test_find_project_not_found(self, mock_execute: MagicMock) -> None: """Test find_project with no results.""" # Mock API response with no projects mock_execute.return_value = (True, []) server = GitLabServer() working_dir = "/test/directory" result = server.find_project("nonexistent-project", working_dir) assert "error" in result assert "not found" in result["error"] mock_execute.assert_called_once_with( [ "api", "/projects?search_namespaces=true&search=nonexistent-project", ], working_dir ) @patch.object(GitLabServer, "execute_glab_command") def test_find_project_api_error(self, mock_execute: MagicMock) -> None: """Test find_project with API error.""" # Mock API error mock_execute.return_value = (False, {"error": "API error"}) server = GitLabServer() working_dir = "/test/directory" result = server.find_project("test-project", working_dir) assert "error" in result assert result["error"] == "API error" mock_execute.assert_called_once_with( ["api", "/projects?search_namespaces=true&search=test-project"], working_dir ) @patch.object(GitLabServer, "execute_glab_command") def test_search_issues_success(self, mock_execute: MagicMock) -> None: """Test successful issue search with default parameters.""" # Mock successful command execution with JSON output mock_execute.return_value = ( True, [ { "id": 1, "iid": 101, "title": "Test Issue 1", "web_url": "https://gitlab.com/group/project/issues/101", "state": "opened", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-02T00:00:00Z", }, { "id": 2, "iid": 102, "title": "Test Issue 2", "web_url": "https://gitlab.com/group/project/issues/102", "state": "closed", "created_at": "2025-01-03T00:00:00Z", "updated_at": "2025-01-04T00:00:00Z", }, ], ) server = GitLabServer() working_dir = "/test/directory" result = server.search_issues(working_directory=working_dir) assert "issues" in result assert len(result["issues"]) == 2 assert result["issues"][0]["id"] == 1 assert result["issues"][0]["title"] == "Test Issue 1" assert result["issues"][1]["id"] == 2 assert result["issues"][1]["title"] == "Test Issue 2" # Verify command was called with correct arguments mock_execute.assert_called_once_with( ["issue", "list", "-O", "json"], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_search_issues_with_filters(self, mock_execute: MagicMock) -> None: """Test issue search with various filters.""" # Mock successful command execution with JSON output mock_execute.return_value = ( True, [ { "id": 1, "iid": 101, "title": "Test Issue 1", "web_url": "https://gitlab.com/group/project/issues/101", "state": "opened", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-02T00:00:00Z", }, ], ) server = GitLabServer() working_dir = "/test/directory" result = server.search_issues( working_directory=working_dir, author="user1", assignee="user2", closed=True, confidential=True, group="test-group", issue_type="incident", iteration=123, label=["bug", "critical"], milestone="v1.0", not_assignee="user3", not_author="user4", not_label=["wontfix"], page=2, per_page=10, project="group/project", ) assert "issues" in result assert len(result["issues"]) == 1 assert result["issues"][0]["id"] == 1 assert result["issues"][0]["title"] == "Test Issue 1" # Verify command was called with correct arguments mock_execute.assert_called_once_with( [ "issue", "list", "-O", "json", "--author", "user1", "-a", "user2", "-c", "-C", "-g", "test-group", "-t", "incident", "-i", "123", "-l", "bug", "-l", "critical", "-m", "v1.0", "--not-assignee", "user3", "--not-author", "user4", "--not-label", "wontfix", "-p", "2", "-P", "10", "-R", "group/project", ], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_search_issues_failure(self, mock_execute: MagicMock) -> None: """Test failed issue search.""" # Mock failed command execution mock_execute.return_value = (False, {"error": "Failed to list issues"}) server = GitLabServer() working_dir = "/test/directory" result = server.search_issues(working_directory=working_dir) assert "error" in result assert result["error"] == "Failed to list issues" mock_execute.assert_called_once_with( ["issue", "list", "-O", "json"], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_search_issues_invalid_json(self, mock_execute: MagicMock) -> None: """Test issue search with invalid JSON response.""" # Mock successful command execution but with invalid JSON mock_execute.return_value = (True, "invalid json") server = GitLabServer() working_dir = "/test/directory" result = server.search_issues(working_directory=working_dir) assert "error" in result assert result["error"] == "Failed to parse issues list" mock_execute.assert_called_once_with( ["issue", "list", "-O", "json"], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_create_issue_success(self, mock_execute: MagicMock) -> None: """Test successful issue creation with required parameters.""" # Mock successful command execution with actual glab output format mock_execute.return_value = (True, """- Creating issue in group/project #1 Test Issue (less than a minute ago) https://gitlab.com/group/project/issues/1""") server = GitLabServer() working_dir = "/test/directory" result = server.create_issue( title="Test Issue", description="Test Description", working_directory=working_dir, ) assert "url" in result assert result["url"] == "https://gitlab.com/group/project/issues/1" mock_execute.assert_called_once_with( ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_create_issue_with_all_params(self, mock_execute: MagicMock) -> None: """Test issue creation with all optional parameters.""" # Mock successful command execution with actual glab output format mock_execute.return_value = (True, """- Creating issue in group/project #2 Test Issue (less than a minute ago) https://gitlab.com/group/project/issues/2""") server = GitLabServer() working_dir = "/test/directory" result = server.create_issue( title="Test Issue", description="Test Description", working_directory=working_dir, labels=["bug", "critical"], assignee=["user1", "user2"], milestone="v1.0", epic_id=123, project="group/project", ) assert "url" in result assert result["url"] == "https://gitlab.com/group/project/issues/2" mock_execute.assert_called_once_with( [ "issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description", "-l", "bug,critical", "-a", "user1", "-a", "user2", "-m", "v1.0", "--epic", "123", "-R", "group/project", ], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_create_issue_failure(self, mock_execute: MagicMock) -> None: """Test failed issue creation.""" # Mock failed command execution mock_execute.return_value = (False, {"error": "Failed to create issue"}) server = GitLabServer() working_dir = "/test/directory" result = server.create_issue( title="Test Issue", description="Test Description", working_directory=working_dir, ) assert "error" in result assert result["error"] == "Failed to create issue" mock_execute.assert_called_once_with( ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"], working_dir, ) @patch.object(GitLabServer, "execute_glab_command") def test_create_issue_invalid_output(self, mock_execute: MagicMock) -> None: """Test issue creation with invalid output format.""" # Mock successful command execution but without a URL in the output mock_execute.return_value = (True, """- Creating issue in group/project #3 Test Issue (less than a minute ago) Invalid URL format""") server = GitLabServer() working_dir = "/test/directory" result = server.create_issue( title="Test Issue", description="Test Description", working_directory=working_dir, ) assert "error" in result assert result["error"] == "Failed to extract issue URL from command output" mock_execute.assert_called_once_with( ["issue", "create", "-y", "-t", "Test Issue", "-d", "Test Description"], working_dir, ) @patch("subprocess.run") def test_working_directory_is_used(self, mock_run: MagicMock) -> None: """Test that the working directory is correctly passed to subprocess.run.""" # Mock successful command execution mock_process = MagicMock() mock_process.returncode = 0 mock_process.stdout = "command output" mock_process.stderr = "" mock_run.return_value = mock_process server = GitLabServer() # Test with different working directories working_dirs = [ "/home/user/project", "/tmp/gitlab", "/var/www/html", ] for working_dir in working_dirs: server.execute_glab_command(["status"], working_dir) mock_run.assert_called_with( ["glab", "status"], capture_output=True, text=True, check=False, cwd=working_dir, ) # Verify the number of calls assert mock_run.call_count == len(working_dirs)