feat: implement Gitea MCP Server with full test coverage
Phase 1 implementation complete: - Complete MCP server with 8 tools (list_issues, get_issue, create_issue, update_issue, add_comment, get_labels, suggest_labels, aggregate_issues) - Hybrid configuration system (system-level + project-level) - Branch-aware security model (main/staging/development) - Mode detection (project vs company/PMO) - Intelligent label suggestion (44-label taxonomy) - 42 unit tests (100% passing) - Comprehensive documentation (README.md, TESTING.md) Files implemented: - mcp_server/config.py - Configuration loader - mcp_server/gitea_client.py - Gitea API client - mcp_server/server.py - MCP server entry point - mcp_server/tools/issues.py - Issue operations - mcp_server/tools/labels.py - Label management - tests/ - Complete test suite (42 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0
mcp-servers/gitea/tests/__init__.py
Normal file
0
mcp-servers/gitea/tests/__init__.py
Normal file
151
mcp-servers/gitea/tests/test_config.py
Normal file
151
mcp-servers/gitea/tests/test_config.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Unit tests for configuration loader.
|
||||
"""
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import os
|
||||
from mcp_server.config import GiteaConfig
|
||||
|
||||
|
||||
def test_load_system_config(tmp_path, monkeypatch):
|
||||
"""Test loading system-level configuration"""
|
||||
# Mock home directory
|
||||
config_dir = tmp_path / '.config' / 'claude'
|
||||
config_dir.mkdir(parents=True)
|
||||
|
||||
config_file = config_dir / 'gitea.env'
|
||||
config_file.write_text(
|
||||
"GITEA_API_URL=https://test.com/api/v1\n"
|
||||
"GITEA_API_TOKEN=test_token\n"
|
||||
"GITEA_OWNER=test_owner\n"
|
||||
)
|
||||
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
config = GiteaConfig()
|
||||
result = config.load()
|
||||
|
||||
assert result['api_url'] == 'https://test.com/api/v1'
|
||||
assert result['api_token'] == 'test_token'
|
||||
assert result['owner'] == 'test_owner'
|
||||
assert result['mode'] == 'company' # No repo specified
|
||||
assert result['repo'] is None
|
||||
|
||||
|
||||
def test_project_config_override(tmp_path, monkeypatch):
|
||||
"""Test that project config overrides system config"""
|
||||
# Set up system config
|
||||
system_config_dir = tmp_path / '.config' / 'claude'
|
||||
system_config_dir.mkdir(parents=True)
|
||||
|
||||
system_config = system_config_dir / 'gitea.env'
|
||||
system_config.write_text(
|
||||
"GITEA_API_URL=https://test.com/api/v1\n"
|
||||
"GITEA_API_TOKEN=test_token\n"
|
||||
"GITEA_OWNER=test_owner\n"
|
||||
)
|
||||
|
||||
# Set up project config
|
||||
project_dir = tmp_path / 'project'
|
||||
project_dir.mkdir()
|
||||
|
||||
project_config = project_dir / '.env'
|
||||
project_config.write_text("GITEA_REPO=test_repo\n")
|
||||
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(project_dir)
|
||||
|
||||
config = GiteaConfig()
|
||||
result = config.load()
|
||||
|
||||
assert result['repo'] == 'test_repo'
|
||||
assert result['mode'] == 'project'
|
||||
|
||||
|
||||
def test_missing_system_config(tmp_path, monkeypatch):
|
||||
"""Test error handling for missing system configuration"""
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(FileNotFoundError) as exc_info:
|
||||
config = GiteaConfig()
|
||||
config.load()
|
||||
|
||||
assert "System config not found" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_missing_required_config(tmp_path, monkeypatch):
|
||||
"""Test error handling for missing required variables"""
|
||||
# Clear environment variables
|
||||
for var in ['GITEA_API_URL', 'GITEA_API_TOKEN', 'GITEA_OWNER', 'GITEA_REPO']:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
# Create incomplete config
|
||||
config_dir = tmp_path / '.config' / 'claude'
|
||||
config_dir.mkdir(parents=True)
|
||||
|
||||
config_file = config_dir / 'gitea.env'
|
||||
config_file.write_text(
|
||||
"GITEA_API_URL=https://test.com/api/v1\n"
|
||||
# Missing GITEA_API_TOKEN and GITEA_OWNER
|
||||
)
|
||||
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
config = GiteaConfig()
|
||||
config.load()
|
||||
|
||||
assert "Missing required configuration" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_mode_detection_project(tmp_path, monkeypatch):
|
||||
"""Test mode detection for project mode"""
|
||||
config_dir = tmp_path / '.config' / 'claude'
|
||||
config_dir.mkdir(parents=True)
|
||||
|
||||
config_file = config_dir / 'gitea.env'
|
||||
config_file.write_text(
|
||||
"GITEA_API_URL=https://test.com/api/v1\n"
|
||||
"GITEA_API_TOKEN=test_token\n"
|
||||
"GITEA_OWNER=test_owner\n"
|
||||
"GITEA_REPO=test_repo\n"
|
||||
)
|
||||
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
config = GiteaConfig()
|
||||
result = config.load()
|
||||
|
||||
assert result['mode'] == 'project'
|
||||
assert result['repo'] == 'test_repo'
|
||||
|
||||
|
||||
def test_mode_detection_company(tmp_path, monkeypatch):
|
||||
"""Test mode detection for company mode (PMO)"""
|
||||
# Clear environment variables, especially GITEA_REPO
|
||||
for var in ['GITEA_API_URL', 'GITEA_API_TOKEN', 'GITEA_OWNER', 'GITEA_REPO']:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
config_dir = tmp_path / '.config' / 'claude'
|
||||
config_dir.mkdir(parents=True)
|
||||
|
||||
config_file = config_dir / 'gitea.env'
|
||||
config_file.write_text(
|
||||
"GITEA_API_URL=https://test.com/api/v1\n"
|
||||
"GITEA_API_TOKEN=test_token\n"
|
||||
"GITEA_OWNER=test_owner\n"
|
||||
# No GITEA_REPO
|
||||
)
|
||||
|
||||
monkeypatch.setenv('HOME', str(tmp_path))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
config = GiteaConfig()
|
||||
result = config.load()
|
||||
|
||||
assert result['mode'] == 'company'
|
||||
assert result['repo'] is None
|
||||
224
mcp-servers/gitea/tests/test_gitea_client.py
Normal file
224
mcp-servers/gitea/tests/test_gitea_client.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Unit tests for Gitea API client.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from mcp_server.gitea_client import GiteaClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Fixture providing mocked configuration"""
|
||||
with patch('mcp_server.gitea_client.GiteaConfig') as mock_cfg:
|
||||
mock_instance = mock_cfg.return_value
|
||||
mock_instance.load.return_value = {
|
||||
'api_url': 'https://test.com/api/v1',
|
||||
'api_token': 'test_token',
|
||||
'owner': 'test_owner',
|
||||
'repo': 'test_repo',
|
||||
'mode': 'project'
|
||||
}
|
||||
yield mock_cfg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gitea_client(mock_config):
|
||||
"""Fixture providing GiteaClient instance with mocked config"""
|
||||
return GiteaClient()
|
||||
|
||||
|
||||
def test_client_initialization(gitea_client):
|
||||
"""Test client initializes with correct configuration"""
|
||||
assert gitea_client.base_url == 'https://test.com/api/v1'
|
||||
assert gitea_client.token == 'test_token'
|
||||
assert gitea_client.owner == 'test_owner'
|
||||
assert gitea_client.repo == 'test_repo'
|
||||
assert gitea_client.mode == 'project'
|
||||
assert 'Authorization' in gitea_client.session.headers
|
||||
assert gitea_client.session.headers['Authorization'] == 'token test_token'
|
||||
|
||||
|
||||
def test_list_issues(gitea_client):
|
||||
"""Test listing issues"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = [
|
||||
{'number': 1, 'title': 'Test Issue 1'},
|
||||
{'number': 2, 'title': 'Test Issue 2'}
|
||||
]
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
issues = gitea_client.list_issues(state='open')
|
||||
|
||||
assert len(issues) == 2
|
||||
assert issues[0]['title'] == 'Test Issue 1'
|
||||
gitea_client.session.get.assert_called_once()
|
||||
|
||||
|
||||
def test_list_issues_with_labels(gitea_client):
|
||||
"""Test listing issues with label filter"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = [{'number': 1, 'title': 'Bug Issue'}]
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
issues = gitea_client.list_issues(state='open', labels=['Type/Bug'])
|
||||
|
||||
gitea_client.session.get.assert_called_once()
|
||||
call_args = gitea_client.session.get.call_args
|
||||
assert call_args[1]['params']['labels'] == 'Type/Bug'
|
||||
|
||||
|
||||
def test_get_issue(gitea_client):
|
||||
"""Test getting specific issue"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {'number': 1, 'title': 'Test Issue'}
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
issue = gitea_client.get_issue(1)
|
||||
|
||||
assert issue['number'] == 1
|
||||
assert issue['title'] == 'Test Issue'
|
||||
|
||||
|
||||
def test_create_issue(gitea_client):
|
||||
"""Test creating new issue"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
'number': 1,
|
||||
'title': 'New Issue',
|
||||
'body': 'Issue body'
|
||||
}
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'post', return_value=mock_response):
|
||||
issue = gitea_client.create_issue(
|
||||
title='New Issue',
|
||||
body='Issue body',
|
||||
labels=['Type/Bug']
|
||||
)
|
||||
|
||||
assert issue['title'] == 'New Issue'
|
||||
gitea_client.session.post.assert_called_once()
|
||||
|
||||
|
||||
def test_update_issue(gitea_client):
|
||||
"""Test updating existing issue"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {
|
||||
'number': 1,
|
||||
'title': 'Updated Issue'
|
||||
}
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'patch', return_value=mock_response):
|
||||
issue = gitea_client.update_issue(
|
||||
issue_number=1,
|
||||
title='Updated Issue'
|
||||
)
|
||||
|
||||
assert issue['title'] == 'Updated Issue'
|
||||
gitea_client.session.patch.assert_called_once()
|
||||
|
||||
|
||||
def test_add_comment(gitea_client):
|
||||
"""Test adding comment to issue"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = {'body': 'Test comment'}
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'post', return_value=mock_response):
|
||||
comment = gitea_client.add_comment(1, 'Test comment')
|
||||
|
||||
assert comment['body'] == 'Test comment'
|
||||
gitea_client.session.post.assert_called_once()
|
||||
|
||||
|
||||
def test_get_labels(gitea_client):
|
||||
"""Test getting repository labels"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = [
|
||||
{'name': 'Type/Bug'},
|
||||
{'name': 'Priority/High'}
|
||||
]
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
labels = gitea_client.get_labels()
|
||||
|
||||
assert len(labels) == 2
|
||||
assert labels[0]['name'] == 'Type/Bug'
|
||||
|
||||
|
||||
def test_get_org_labels(gitea_client):
|
||||
"""Test getting organization labels"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = [
|
||||
{'name': 'Type/Bug'},
|
||||
{'name': 'Type/Feature'}
|
||||
]
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
labels = gitea_client.get_org_labels()
|
||||
|
||||
assert len(labels) == 2
|
||||
|
||||
|
||||
def test_list_repos(gitea_client):
|
||||
"""Test listing organization repositories (PMO mode)"""
|
||||
mock_response = Mock()
|
||||
mock_response.json.return_value = [
|
||||
{'name': 'repo1'},
|
||||
{'name': 'repo2'}
|
||||
]
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch.object(gitea_client.session, 'get', return_value=mock_response):
|
||||
repos = gitea_client.list_repos()
|
||||
|
||||
assert len(repos) == 2
|
||||
assert repos[0]['name'] == 'repo1'
|
||||
|
||||
|
||||
def test_aggregate_issues(gitea_client):
|
||||
"""Test aggregating issues across repositories (PMO mode)"""
|
||||
# Mock list_repos
|
||||
gitea_client.list_repos = Mock(return_value=[
|
||||
{'name': 'repo1'},
|
||||
{'name': 'repo2'}
|
||||
])
|
||||
|
||||
# Mock list_issues
|
||||
gitea_client.list_issues = Mock(side_effect=[
|
||||
[{'number': 1, 'title': 'Issue 1'}], # repo1
|
||||
[{'number': 2, 'title': 'Issue 2'}] # repo2
|
||||
])
|
||||
|
||||
aggregated = gitea_client.aggregate_issues(state='open')
|
||||
|
||||
assert 'repo1' in aggregated
|
||||
assert 'repo2' in aggregated
|
||||
assert len(aggregated['repo1']) == 1
|
||||
assert len(aggregated['repo2']) == 1
|
||||
|
||||
|
||||
def test_no_repo_specified_error(gitea_client):
|
||||
"""Test error when repository not specified"""
|
||||
# Create client without repo
|
||||
with patch('mcp_server.gitea_client.GiteaConfig') as mock_cfg:
|
||||
mock_instance = mock_cfg.return_value
|
||||
mock_instance.load.return_value = {
|
||||
'api_url': 'https://test.com/api/v1',
|
||||
'api_token': 'test_token',
|
||||
'owner': 'test_owner',
|
||||
'repo': None, # No repo
|
||||
'mode': 'company'
|
||||
}
|
||||
client = GiteaClient()
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
client.list_issues()
|
||||
|
||||
assert "Repository not specified" in str(exc_info.value)
|
||||
159
mcp-servers/gitea/tests/test_issues.py
Normal file
159
mcp-servers/gitea/tests/test_issues.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Unit tests for issue tools with branch detection.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from mcp_server.tools.issues import IssueTools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gitea_client():
|
||||
"""Fixture providing mocked Gitea client"""
|
||||
client = Mock()
|
||||
client.mode = 'project'
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def issue_tools(mock_gitea_client):
|
||||
"""Fixture providing IssueTools instance"""
|
||||
return IssueTools(mock_gitea_client)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_issues_development_branch(issue_tools):
|
||||
"""Test listing issues on development branch (allowed)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='feat/test-feature'):
|
||||
issue_tools.gitea.list_issues = Mock(return_value=[{'number': 1}])
|
||||
|
||||
issues = await issue_tools.list_issues(state='open')
|
||||
|
||||
assert len(issues) == 1
|
||||
issue_tools.gitea.list_issues.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_development_branch(issue_tools):
|
||||
"""Test creating issue on development branch (allowed)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
|
||||
issue_tools.gitea.create_issue = Mock(return_value={'number': 1})
|
||||
|
||||
issue = await issue_tools.create_issue('Test', 'Body')
|
||||
|
||||
assert issue['number'] == 1
|
||||
issue_tools.gitea.create_issue.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_main_branch_blocked(issue_tools):
|
||||
"""Test creating issue on main branch (blocked)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='main'):
|
||||
with pytest.raises(PermissionError) as exc_info:
|
||||
await issue_tools.create_issue('Test', 'Body')
|
||||
|
||||
assert "Cannot create issues on branch 'main'" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_issue_staging_branch_allowed(issue_tools):
|
||||
"""Test creating issue on staging branch (allowed for documentation)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='staging'):
|
||||
issue_tools.gitea.create_issue = Mock(return_value={'number': 1})
|
||||
|
||||
issue = await issue_tools.create_issue('Test', 'Body')
|
||||
|
||||
assert issue['number'] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_issue_main_branch_blocked(issue_tools):
|
||||
"""Test updating issue on main branch (blocked)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='main'):
|
||||
with pytest.raises(PermissionError) as exc_info:
|
||||
await issue_tools.update_issue(1, title='Updated')
|
||||
|
||||
assert "Cannot update issues on branch 'main'" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_issues_main_branch_allowed(issue_tools):
|
||||
"""Test listing issues on main branch (allowed - read-only)"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='main'):
|
||||
issue_tools.gitea.list_issues = Mock(return_value=[{'number': 1}])
|
||||
|
||||
issues = await issue_tools.list_issues(state='open')
|
||||
|
||||
assert len(issues) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_issue(issue_tools):
|
||||
"""Test getting specific issue"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
|
||||
issue_tools.gitea.get_issue = Mock(return_value={'number': 1, 'title': 'Test'})
|
||||
|
||||
issue = await issue_tools.get_issue(1)
|
||||
|
||||
assert issue['number'] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_comment(issue_tools):
|
||||
"""Test adding comment to issue"""
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
|
||||
issue_tools.gitea.add_comment = Mock(return_value={'body': 'Test comment'})
|
||||
|
||||
comment = await issue_tools.add_comment(1, 'Test comment')
|
||||
|
||||
assert comment['body'] == 'Test comment'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aggregate_issues_company_mode(issue_tools):
|
||||
"""Test aggregating issues in company mode"""
|
||||
issue_tools.gitea.mode = 'company'
|
||||
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
|
||||
issue_tools.gitea.aggregate_issues = Mock(return_value={
|
||||
'repo1': [{'number': 1}],
|
||||
'repo2': [{'number': 2}]
|
||||
})
|
||||
|
||||
aggregated = await issue_tools.aggregate_issues()
|
||||
|
||||
assert 'repo1' in aggregated
|
||||
assert 'repo2' in aggregated
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aggregate_issues_project_mode_error(issue_tools):
|
||||
"""Test that aggregate_issues fails in project mode"""
|
||||
issue_tools.gitea.mode = 'project'
|
||||
|
||||
with patch.object(issue_tools, '_get_current_branch', return_value='development'):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await issue_tools.aggregate_issues()
|
||||
|
||||
assert "only available in company mode" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_branch_detection():
|
||||
"""Test branch detection logic"""
|
||||
tools = IssueTools(Mock())
|
||||
|
||||
# Test development branches
|
||||
with patch.object(tools, '_get_current_branch', return_value='development'):
|
||||
assert tools._check_branch_permissions('create_issue') is True
|
||||
|
||||
with patch.object(tools, '_get_current_branch', return_value='feat/new-feature'):
|
||||
assert tools._check_branch_permissions('create_issue') is True
|
||||
|
||||
# Test production branches
|
||||
with patch.object(tools, '_get_current_branch', return_value='main'):
|
||||
assert tools._check_branch_permissions('create_issue') is False
|
||||
assert tools._check_branch_permissions('list_issues') is True
|
||||
|
||||
# Test staging branches
|
||||
with patch.object(tools, '_get_current_branch', return_value='staging'):
|
||||
assert tools._check_branch_permissions('create_issue') is True
|
||||
assert tools._check_branch_permissions('update_issue') is False
|
||||
246
mcp-servers/gitea/tests/test_labels.py
Normal file
246
mcp-servers/gitea/tests/test_labels.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Unit tests for label tools with suggestion logic.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from mcp_server.tools.labels import LabelTools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gitea_client():
|
||||
"""Fixture providing mocked Gitea client"""
|
||||
client = Mock()
|
||||
client.repo = 'test_repo'
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def label_tools(mock_gitea_client):
|
||||
"""Fixture providing LabelTools instance"""
|
||||
return LabelTools(mock_gitea_client)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_labels(label_tools):
|
||||
"""Test getting all labels (org + repo)"""
|
||||
label_tools.gitea.get_org_labels = Mock(return_value=[
|
||||
{'name': 'Type/Bug'},
|
||||
{'name': 'Type/Feature'}
|
||||
])
|
||||
label_tools.gitea.get_labels = Mock(return_value=[
|
||||
{'name': 'Component/Backend'},
|
||||
{'name': 'Component/Frontend'}
|
||||
])
|
||||
|
||||
result = await label_tools.get_labels()
|
||||
|
||||
assert len(result['organization']) == 2
|
||||
assert len(result['repository']) == 2
|
||||
assert result['total_count'] == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_bug():
|
||||
"""Test label suggestion for bug context"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
context = "Fix critical bug in login authentication"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
|
||||
assert 'Type/Bug' in suggestions
|
||||
assert 'Priority/Critical' in suggestions
|
||||
assert 'Component/Auth' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_feature():
|
||||
"""Test label suggestion for feature context"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
context = "Add new feature to implement user dashboard"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
|
||||
assert 'Type/Feature' in suggestions
|
||||
assert any('Priority' in label for label in suggestions)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_refactor():
|
||||
"""Test label suggestion for refactor context"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
context = "Refactor architecture to extract service layer"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
|
||||
assert 'Type/Refactor' in suggestions
|
||||
assert 'Component/Backend' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_documentation():
|
||||
"""Test label suggestion for documentation context"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
context = "Update documentation for API endpoints"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
|
||||
assert 'Type/Documentation' in suggestions
|
||||
assert 'Component/API' in suggestions or 'Component/Docs' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_priority():
|
||||
"""Test priority detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# Critical priority
|
||||
context = "Urgent blocker in production"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Priority/Critical' in suggestions
|
||||
|
||||
# High priority
|
||||
context = "Important feature needed asap"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Priority/High' in suggestions
|
||||
|
||||
# Low priority
|
||||
context = "Nice-to-have optional improvement"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Priority/Low' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_complexity():
|
||||
"""Test complexity detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# Simple complexity
|
||||
context = "Simple quick fix for typo"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Complexity/Simple' in suggestions
|
||||
|
||||
# Complex complexity
|
||||
context = "Complex challenging architecture redesign"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Complexity/Complex' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_efforts():
|
||||
"""Test efforts detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# XS effort
|
||||
context = "Tiny fix that takes 1 hour"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Efforts/XS' in suggestions
|
||||
|
||||
# L effort
|
||||
context = "Large feature taking 1 week"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Efforts/L' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_components():
|
||||
"""Test component detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# Backend component
|
||||
context = "Update backend API service"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Component/Backend' in suggestions
|
||||
assert 'Component/API' in suggestions
|
||||
|
||||
# Frontend component
|
||||
context = "Fix frontend UI component"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Component/Frontend' in suggestions
|
||||
|
||||
# Database component
|
||||
context = "Add database migration for schema"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Component/Database' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_tech_stack():
|
||||
"""Test tech stack detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# Python
|
||||
context = "Update Python FastAPI endpoint"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Tech/Python' in suggestions
|
||||
assert 'Tech/FastAPI' in suggestions
|
||||
|
||||
# Docker
|
||||
context = "Fix Dockerfile configuration"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Tech/Docker' in suggestions
|
||||
|
||||
# PostgreSQL
|
||||
context = "Optimize PostgreSQL query"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Tech/PostgreSQL' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_source():
|
||||
"""Test source detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# Development
|
||||
context = "Issue found in development environment"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Source/Development' in suggestions
|
||||
|
||||
# Production
|
||||
context = "Critical production issue"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Source/Production' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_risk():
|
||||
"""Test risk detection in suggestions"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
# High risk
|
||||
context = "Breaking change to major API"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Risk/High' in suggestions
|
||||
|
||||
# Low risk
|
||||
context = "Safe minor update with low risk"
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
assert 'Risk/Low' in suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_labels_multiple_categories():
|
||||
"""Test that suggestions span multiple categories"""
|
||||
tools = LabelTools(Mock())
|
||||
|
||||
context = """
|
||||
Urgent critical bug in production backend API service.
|
||||
Need to fix broken authentication endpoint.
|
||||
This is a complex issue requiring FastAPI and PostgreSQL expertise.
|
||||
"""
|
||||
|
||||
suggestions = await tools.suggest_labels(context)
|
||||
|
||||
# Should have Type
|
||||
assert any('Type/' in label for label in suggestions)
|
||||
|
||||
# Should have Priority
|
||||
assert any('Priority/' in label for label in suggestions)
|
||||
|
||||
# Should have Component
|
||||
assert any('Component/' in label for label in suggestions)
|
||||
|
||||
# Should have Tech
|
||||
assert any('Tech/' in label for label in suggestions)
|
||||
|
||||
# Should have Source
|
||||
assert any('Source/' in label for label in suggestions)
|
||||
Reference in New Issue
Block a user