test_shareholder_info.pyβ’13.2 kB
"""
Unit tests for Shareholder Info functionality
TDD Red Phase: Write failing tests for shareholder analysis
"""
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime, date, timedelta
from typing import Any, Dict, List, Optional
# Test imports - initially will fail (TDD Red phase)
from src.server import MCPStockDetailsServer
from src.tools.shareholder_tools import ShareholderAnalyzer
from src.exceptions import MCPStockDetailsError, InsufficientDataError
class TestShareholderInfo:
"""Test cases for shareholder info functionality"""
@pytest.fixture
def server_with_shareholder(self):
"""Create server instance with shareholder analyzer"""
server = MCPStockDetailsServer()
# This will fail initially - Red phase
server.shareholder_analyzer = ShareholderAnalyzer()
return server
@pytest.fixture
def sample_shareholder_data(self):
"""Sample shareholder data for testing"""
return {
"005930": { # Samsung Electronics
"major_shareholders": [
{
"name": "κ΅λ―Όμ°κΈκ³΅λ¨",
"shares": 148_712_953,
"percentage": 9.57,
"type": "institutional"
},
{
"name": "μΌμ±λ¬Όμ° μ£Όμνμ¬",
"shares": 133_284_628,
"percentage": 8.58,
"type": "corporate"
},
{
"name": "μ΄μ¬μ©",
"shares": 18_352_174,
"percentage": 1.18,
"type": "individual"
}
],
"ownership_structure": {
"free_float": 82.45,
"foreign_ownership": 52.37,
"institutional_ownership": 31.28,
"individual_ownership": 16.35
},
"dividend_history": [
{"year": 2023, "dividend_per_share": 1640, "dividend_yield": 2.18, "payout_ratio": 23.5},
{"year": 2022, "dividend_per_share": 1444, "dividend_yield": 2.89, "payout_ratio": 28.7},
{"year": 2021, "dividend_per_share": 1378, "dividend_yield": 1.95, "payout_ratio": 22.1}
],
"governance_metrics": {
"board_independence": 45.5,
"audit_committee_independence": 100.0,
"compensation_committee_independence": 75.0,
"board_diversity_ratio": 18.2,
"avg_board_tenure": 4.2
}
}
}
@pytest.mark.asyncio
async def test_get_shareholder_info_tool_registration(self, server_with_shareholder):
"""Test that get_shareholder_info tool is properly registered"""
tools = await server_with_shareholder.list_tools()
tool_names = [tool.name for tool in tools]
assert "get_shareholder_info" in tool_names
# Check tool description and parameters
shareholder_tool = next(tool for tool in tools if tool.name == "get_shareholder_info")
assert "shareholder" in shareholder_tool.description.lower()
assert "ownership" in shareholder_tool.description.lower()
assert "company_code" in shareholder_tool.inputSchema["properties"]
@pytest.mark.asyncio
async def test_major_shareholders_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test major shareholders analysis"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_major_shareholders": True
})
assert result is not None
assert len(result) > 0
content = result[0].text
# Should include major shareholders
assert "MAJOR SHAREHOLDERS" in content
assert "κ΅λ―Όμ°κΈκ³΅λ¨" in content
assert "μΌμ±λ¬Όμ°" in content
assert "9.57%" in content or "9.6%" in content
assert "8.58%" in content or "8.6%" in content
@pytest.mark.asyncio
async def test_ownership_structure_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test ownership structure analysis"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_ownership_structure": True
})
assert result is not None
content = result[0].text
# Should include ownership structure
assert "OWNERSHIP STRUCTURE" in content
assert "Free Float" in content
assert "Foreign Ownership" in content
assert "Institutional" in content
assert "82.45%" in content or "82.5%" in content
assert "52.37%" in content or "52.4%" in content
@pytest.mark.asyncio
async def test_dividend_history_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test dividend history analysis"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_dividend_history": True
})
assert result is not None
content = result[0].text
# Should include dividend history
assert "DIVIDEND HISTORY" in content
assert "2023" in content
assert "1640" in content or "β©1,640" in content
assert "2.18%" in content
assert "Payout Ratio" in content
@pytest.mark.asyncio
async def test_governance_metrics_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test governance metrics analysis"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_governance_metrics": True
})
assert result is not None
content = result[0].text
# Should include governance metrics
assert "GOVERNANCE METRICS" in content
assert "Board Independence" in content
assert "Audit Committee" in content
assert "45.5%" in content
assert "100.0%" in content
@pytest.mark.asyncio
async def test_shareholder_concentration_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test shareholder concentration analysis"""
company_code = "005930"
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_concentration_analysis": True
})
assert result is not None
content = result[0].text
# Should include concentration analysis
assert "CONCENTRATION ANALYSIS" in content
assert "HHI" in content or "Herfindahl" in content
assert "Top 5 Concentration" in content
assert "Top 10 Concentration" in content
@pytest.mark.asyncio
async def test_voting_rights_analysis(self, server_with_shareholder):
"""Test voting rights analysis"""
company_code = "005930"
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_voting_rights": True
})
assert result is not None
content = result[0].text
# Should include voting rights analysis
assert "VOTING RIGHTS" in content
assert "Voting Power" in content
assert "Control Assessment" in content
@pytest.mark.asyncio
async def test_insider_trading_analysis(self, server_with_shareholder):
"""Test insider trading analysis"""
company_code = "005930"
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_insider_trading": True
})
assert result is not None
content = result[0].text
# Should include insider trading analysis
assert "INSIDER TRADING" in content
assert "Recent Transactions" in content
assert "Net Buy/Sell" in content
@pytest.mark.asyncio
async def test_comprehensive_shareholder_report(self, server_with_shareholder, sample_shareholder_data):
"""Test comprehensive shareholder report generation"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"analysis_type": "comprehensive"
})
assert result is not None
content = result[0].text
# Should include comprehensive analysis
assert "COMPREHENSIVE SHAREHOLDER ANALYSIS" in content
assert "Ownership Summary" in content
assert "Key Insights" in content
assert "Governance Assessment" in content
@pytest.mark.asyncio
async def test_shareholder_change_tracking(self, server_with_shareholder):
"""Test shareholder change tracking"""
company_code = "005930"
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_change_tracking": True,
"tracking_period": "1Y"
})
assert result is not None
content = result[0].text
# Should include change tracking
assert "SHAREHOLDER CHANGES" in content
assert "Period" in content
assert "Net Change" in content
assert "New Entries" in content
assert "Exits" in content
@pytest.mark.asyncio
async def test_dividend_sustainability_analysis(self, server_with_shareholder, sample_shareholder_data):
"""Test dividend sustainability analysis"""
company_code = "005930"
with patch.object(server_with_shareholder.shareholder_analyzer, 'get_shareholder_data') as mock_data:
mock_data.return_value = sample_shareholder_data[company_code]
result = await server_with_shareholder._handle_get_shareholder_info({
"company_code": company_code,
"include_dividend_sustainability": True
})
assert result is not None
content = result[0].text
# Should include dividend sustainability
assert "DIVIDEND SUSTAINABILITY" in content
assert "Coverage Ratio" in content
assert "Sustainability Score" in content
assert "Future Outlook" in content
@pytest.mark.asyncio
async def test_shareholder_info_error_handling(self, server_with_shareholder):
"""Test error handling for shareholder info"""
# Test invalid company code
with pytest.raises(MCPStockDetailsError):
await server_with_shareholder._handle_get_shareholder_info({
"company_code": "",
"include_major_shareholders": True
})
# Test insufficient shareholder data
with pytest.raises(InsufficientDataError):
await server_with_shareholder._handle_get_shareholder_info({
"company_code": "999999", # Non-existent company
"include_major_shareholders": True
})