943a03ae5a
Replace broad assertion (mean > 0) with specific expected value using pytest.approx for floating-point comparison. This makes the test more robust and prevents future regressions. Changes: - Change area_stats mean assertion from > 0 to == pytest.approx(80.0) - Add final newline to file per convention Expected mean: (80 + 90 + 70) / 3 = 80.0 Addresses PR #2 review comments on line 527. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
528 lines
20 KiB
Python
528 lines
20 KiB
Python
"""
|
|
Tests for the GovmapClient class.
|
|
"""
|
|
|
|
import pytest
|
|
import requests
|
|
from unittest.mock import Mock, patch
|
|
from nadlan_mcp.govmap import GovmapClient
|
|
|
|
|
|
class TestGovmapClient:
|
|
"""Test cases for GovmapClient class."""
|
|
|
|
def test_client_initialization(self):
|
|
"""Test that GovmapClient initializes correctly."""
|
|
client = GovmapClient()
|
|
assert client.base_url == "https://www.govmap.gov.il/api"
|
|
assert client.session is not None
|
|
assert client.session.headers['Content-Type'] == 'application/json'
|
|
assert client.session.headers['User-Agent'] == 'NadlanMCP/1.0.0'
|
|
|
|
def test_client_initialization_with_custom_url(self):
|
|
"""Test that GovmapClient can be initialized with custom URL."""
|
|
custom_url = "https://custom-api.example.com/api/"
|
|
client = GovmapClient(custom_url)
|
|
assert client.base_url == "https://custom-api.example.com/api"
|
|
|
|
@patch('requests.Session')
|
|
def test_autocomplete_address_success(self, mock_session_class):
|
|
"""Test successful address autocomplete."""
|
|
# Mock response
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"resultsCount": 1,
|
|
"results": [
|
|
{
|
|
"id": "address|ADDR|123|test",
|
|
"text": "תל אביב",
|
|
"type": "address",
|
|
"score": 100,
|
|
"shape": "POINT(3870000.123 3770000.456)",
|
|
"data": {}
|
|
}
|
|
]
|
|
}
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.post.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
result = client.autocomplete_address("תל אביב")
|
|
|
|
assert result["resultsCount"] == 1
|
|
assert len(result["results"]) == 1
|
|
assert result["results"][0]["text"] == "תל אביב"
|
|
mock_session.post.assert_called_once()
|
|
|
|
@patch('requests.Session')
|
|
def test_autocomplete_address_empty_results(self, mock_session_class):
|
|
"""Test autocomplete with empty results - should return empty results, not raise error."""
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {"resultsCount": 0, "results": []}
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.post.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
result = client.autocomplete_address("nonexistent")
|
|
|
|
assert result["resultsCount"] == 0
|
|
assert len(result["results"]) == 0
|
|
|
|
@patch('requests.Session')
|
|
def test_autocomplete_address_invalid_response(self, mock_session_class):
|
|
"""Test autocomplete with truly invalid response format."""
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {"invalid": "response"} # Missing 'results' key
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.post.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
|
|
with pytest.raises(ValueError, match="Invalid response format"):
|
|
client.autocomplete_address("test")
|
|
|
|
def test_coordinate_parsing_from_wkt_point(self):
|
|
"""Test coordinate parsing from WKT POINT format."""
|
|
client = GovmapClient()
|
|
|
|
# Mock the autocomplete response with WKT POINT
|
|
mock_autocomplete_result = {
|
|
"results": [
|
|
{
|
|
"shape": "POINT(3870000.123 3770000.456)",
|
|
"text": "test address"
|
|
}
|
|
]
|
|
}
|
|
|
|
# We'll test the coordinate parsing logic by calling the method that uses it
|
|
with patch.object(client, 'autocomplete_address', return_value=mock_autocomplete_result):
|
|
with patch.object(client, 'get_deals_by_radius', return_value=[]):
|
|
with patch.object(client, 'get_street_deals', return_value=[]):
|
|
with patch.object(client, 'get_neighborhood_deals', return_value=[]):
|
|
result = client.find_recent_deals_for_address("test", years_back=1)
|
|
assert result == []
|
|
|
|
@patch('requests.Session')
|
|
def test_get_deals_by_radius_success(self, mock_session_class):
|
|
"""Test successful deals by radius query."""
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = [
|
|
{
|
|
"dealscount": "2",
|
|
"settlementNameHeb": "תל אביב-יפו",
|
|
"polygon_id": "123-456",
|
|
"objectid": 12345
|
|
}
|
|
]
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.get.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
result = client.get_deals_by_radius((3870000.123, 3770000.456), radius=50)
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["polygon_id"] == "123-456"
|
|
mock_session.get.assert_called_once()
|
|
|
|
@patch('requests.Session')
|
|
def test_get_street_deals_success(self, mock_session_class):
|
|
"""Test successful street deals query."""
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"totalCount": "1",
|
|
"data": [
|
|
{
|
|
"objectid": 123,
|
|
"dealAmount": 1000000,
|
|
"dealDate": "2025-01-01T00:00:00.000Z",
|
|
"assetArea": 100,
|
|
"settlementNameHeb": "תל אביב-יפו",
|
|
"propertyTypeDescription": "דירה"
|
|
}
|
|
]
|
|
}
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.get.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
result = client.get_street_deals("123-456")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["dealAmount"] == 1000000
|
|
assert result[0]["assetArea"] == 100
|
|
mock_session.get.assert_called_once()
|
|
|
|
@patch('requests.Session')
|
|
def test_get_neighborhood_deals_success(self, mock_session_class):
|
|
"""Test successful neighborhood deals query."""
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"totalCount": "1",
|
|
"data": [
|
|
{
|
|
"objectid": 456,
|
|
"dealAmount": 2000000,
|
|
"dealDate": "2025-01-15T00:00:00.000Z",
|
|
"assetArea": 120,
|
|
"settlementNameHeb": "תל אביב-יפו",
|
|
"propertyTypeDescription": "דירה"
|
|
}
|
|
]
|
|
}
|
|
mock_response.raise_for_status.return_value = None
|
|
|
|
mock_session = Mock()
|
|
mock_session.get.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
result = client.get_neighborhood_deals("123-456")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["dealAmount"] == 2000000
|
|
assert result[0]["assetArea"] == 120
|
|
mock_session.get.assert_called_once()
|
|
|
|
@patch('nadlan_mcp.main.GovmapClient.get_neighborhood_deals')
|
|
@patch('nadlan_mcp.main.GovmapClient.get_street_deals')
|
|
@patch('nadlan_mcp.main.GovmapClient.get_deals_by_radius')
|
|
@patch('nadlan_mcp.main.GovmapClient.autocomplete_address')
|
|
def test_find_recent_deals_for_address_integration(self, mock_autocomplete, mock_radius, mock_street, mock_neighborhood):
|
|
"""Test the main integration function."""
|
|
# Mock autocomplete response
|
|
mock_autocomplete.return_value = {
|
|
"results": [
|
|
{
|
|
"shape": "POINT(3870000.123 3770000.456)",
|
|
"text": "test address"
|
|
}
|
|
]
|
|
}
|
|
|
|
# Mock radius response
|
|
mock_radius.return_value = [
|
|
{"polygon_id": "123-456", "objectid": 1}
|
|
]
|
|
|
|
# Mock street deals response
|
|
mock_street.return_value = [
|
|
{
|
|
"dealId": "deal1",
|
|
"dealAmount": 1000000,
|
|
"dealDate": "2025-01-01T00:00:00.000Z",
|
|
"address": "Test Street 1"
|
|
}
|
|
]
|
|
|
|
# Mock neighborhood deals response
|
|
mock_neighborhood.return_value = [
|
|
{
|
|
"dealId": "deal2",
|
|
"dealAmount": 2000000,
|
|
"dealDate": "2025-01-15T00:00:00.000Z",
|
|
"address": "Test Street 2"
|
|
}
|
|
]
|
|
|
|
client = GovmapClient()
|
|
result = client.find_recent_deals_for_address("test address", years_back=1)
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["dealDate"] == "2025-01-15T00:00:00.000Z" # Should be sorted by date
|
|
assert result[1]["dealDate"] == "2025-01-01T00:00:00.000Z"
|
|
|
|
@patch('requests.Session')
|
|
def test_http_error_handling(self, mock_session_class):
|
|
"""Test that HTTP errors are properly handled."""
|
|
mock_response = Mock()
|
|
mock_response.raise_for_status.side_effect = Exception("HTTP Error")
|
|
|
|
mock_session = Mock()
|
|
mock_session.post.return_value = mock_response
|
|
mock_session_class.return_value = mock_session
|
|
|
|
client = GovmapClient()
|
|
|
|
with pytest.raises(Exception, match="HTTP Error"):
|
|
client.autocomplete_address("test")
|
|
|
|
def test_invalid_coordinate_format(self):
|
|
"""Test handling of invalid coordinate formats."""
|
|
client = GovmapClient()
|
|
|
|
# Mock autocomplete response with invalid shape
|
|
mock_autocomplete_result = {
|
|
"results": [
|
|
{
|
|
"shape": "INVALID_FORMAT",
|
|
"text": "test address"
|
|
}
|
|
]
|
|
}
|
|
|
|
with patch.object(client, 'autocomplete_address', return_value=mock_autocomplete_result):
|
|
with pytest.raises(ValueError, match="Invalid coordinate format"):
|
|
client.find_recent_deals_for_address("test", years_back=1)
|
|
|
|
|
|
class TestMarketAnalysisFunctions:
|
|
"""Test cases for market analysis functions."""
|
|
|
|
def test_calculate_market_activity_score_success(self):
|
|
"""Test successful market activity score calculation."""
|
|
client = GovmapClient()
|
|
|
|
# Sample deals with dates
|
|
deals = [
|
|
{"dealDate": "2023-01-15", "dealAmount": 1000000},
|
|
{"dealDate": "2023-01-20", "dealAmount": 1100000},
|
|
{"dealDate": "2023-02-10", "dealAmount": 1200000},
|
|
{"dealDate": "2023-03-05", "dealAmount": 1150000},
|
|
{"dealDate": "2023-04-12", "dealAmount": 1250000},
|
|
]
|
|
|
|
result = client.calculate_market_activity_score(deals)
|
|
|
|
assert "total_deals" in result
|
|
assert "deals_per_month" in result
|
|
assert "activity_score" in result
|
|
assert "activity_level" in result
|
|
assert "trend" in result
|
|
assert "monthly_distribution" in result
|
|
assert result["total_deals"] == 5
|
|
assert result["deals_per_month"] > 0
|
|
assert 0 <= result["activity_score"] <= 100
|
|
|
|
def test_calculate_market_activity_score_empty_deals(self):
|
|
"""Test market activity score with empty deals list."""
|
|
client = GovmapClient()
|
|
|
|
with pytest.raises(ValueError, match="Cannot calculate market activity from empty deals list"):
|
|
client.calculate_market_activity_score([])
|
|
|
|
def test_calculate_market_activity_score_invalid_dates(self):
|
|
"""Test market activity score with invalid dates."""
|
|
client = GovmapClient()
|
|
|
|
# Deals with invalid dates
|
|
deals = [
|
|
{"dealDate": "", "dealAmount": 1000000},
|
|
{"dealAmount": 1100000}, # Missing dealDate
|
|
]
|
|
|
|
with pytest.raises(ValueError, match="No valid deal dates found"):
|
|
client.calculate_market_activity_score(deals)
|
|
|
|
def test_calculate_market_activity_score_high_activity(self):
|
|
"""Test market activity score with high activity."""
|
|
client = GovmapClient()
|
|
|
|
# Generate many deals in short period (high activity)
|
|
deals = [
|
|
{"dealDate": f"2023-01-{i:02d}", "dealAmount": 1000000 + i * 10000}
|
|
for i in range(1, 31) # 30 deals in one month
|
|
]
|
|
|
|
result = client.calculate_market_activity_score(deals)
|
|
|
|
assert result["activity_level"] == "very_high"
|
|
assert result["activity_score"] >= 90
|
|
|
|
def test_analyze_investment_potential_success(self):
|
|
"""Test successful investment potential analysis."""
|
|
client = GovmapClient()
|
|
|
|
# Sample deals with price appreciation
|
|
deals = [
|
|
{"dealDate": "2022-01-15", "dealAmount": 1000000, "assetArea": 80, "price_per_sqm": 12500},
|
|
{"dealDate": "2022-06-10", "dealAmount": 1050000, "assetArea": 80, "price_per_sqm": 13125},
|
|
{"dealDate": "2023-01-05", "dealAmount": 1100000, "assetArea": 80, "price_per_sqm": 13750},
|
|
{"dealDate": "2023-06-12", "dealAmount": 1150000, "assetArea": 80, "price_per_sqm": 14375},
|
|
]
|
|
|
|
result = client.analyze_investment_potential(deals)
|
|
|
|
assert "price_appreciation_rate" in result
|
|
assert "price_volatility" in result
|
|
assert "market_stability" in result
|
|
assert "price_trend" in result
|
|
assert "avg_price_per_sqm" in result
|
|
assert "investment_score" in result
|
|
assert "data_quality" in result
|
|
assert 0 <= result["investment_score"] <= 100
|
|
assert result["price_trend"] in ["increasing", "stable", "decreasing"]
|
|
|
|
def test_analyze_investment_potential_empty_deals(self):
|
|
"""Test investment potential with empty deals list."""
|
|
client = GovmapClient()
|
|
|
|
with pytest.raises(ValueError, match="Cannot analyze investment potential from empty deals list"):
|
|
client.analyze_investment_potential([])
|
|
|
|
def test_analyze_investment_potential_insufficient_data(self):
|
|
"""Test investment potential with insufficient data."""
|
|
client = GovmapClient()
|
|
|
|
# Only 2 deals (need at least 3)
|
|
deals = [
|
|
{"dealDate": "2023-01-15", "dealAmount": 1000000, "assetArea": 80, "price_per_sqm": 12500},
|
|
{"dealDate": "2023-06-10", "dealAmount": 1050000, "assetArea": 80, "price_per_sqm": 13125},
|
|
]
|
|
|
|
with pytest.raises(ValueError, match="Insufficient data for investment analysis"):
|
|
client.analyze_investment_potential(deals)
|
|
|
|
def test_analyze_investment_potential_stable_market(self):
|
|
"""Test investment potential with stable market (low volatility)."""
|
|
client = GovmapClient()
|
|
|
|
# Deals with consistent prices (very stable)
|
|
deals = [
|
|
{"dealDate": f"2023-{i:02d}-15", "dealAmount": 1000000 + i * 1000, "assetArea": 80, "price_per_sqm": 12500 + i * 12.5}
|
|
for i in range(1, 13) # 12 months, slight increase
|
|
]
|
|
|
|
result = client.analyze_investment_potential(deals)
|
|
|
|
assert result["market_stability"] in ["very_stable", "stable"]
|
|
assert result["price_volatility"] < 50
|
|
|
|
def test_get_market_liquidity_success(self):
|
|
"""Test successful market liquidity calculation."""
|
|
client = GovmapClient()
|
|
|
|
# Sample deals across multiple quarters
|
|
deals = [
|
|
{"dealDate": "2023-01-15", "dealAmount": 1000000},
|
|
{"dealDate": "2023-02-20", "dealAmount": 1100000},
|
|
{"dealDate": "2023-05-10", "dealAmount": 1200000},
|
|
{"dealDate": "2023-06-05", "dealAmount": 1150000},
|
|
{"dealDate": "2023-09-12", "dealAmount": 1250000},
|
|
{"dealDate": "2023-10-18", "dealAmount": 1300000},
|
|
]
|
|
|
|
result = client.get_market_liquidity(deals)
|
|
|
|
assert "total_deals" in result
|
|
assert "deals_per_month" in result
|
|
assert "deals_per_quarter" in result
|
|
assert "quarterly_breakdown" in result
|
|
assert "monthly_breakdown" in result
|
|
assert "velocity_score" in result
|
|
assert "liquidity_rating" in result
|
|
assert "trend_direction" in result
|
|
assert "most_active_period" in result
|
|
assert result["total_deals"] == 6
|
|
assert 0 <= result["velocity_score"] <= 100
|
|
|
|
def test_get_market_liquidity_empty_deals(self):
|
|
"""Test market liquidity with empty deals list."""
|
|
client = GovmapClient()
|
|
|
|
with pytest.raises(ValueError, match="Cannot calculate market liquidity from empty deals list"):
|
|
client.get_market_liquidity([])
|
|
|
|
def test_get_market_liquidity_quarterly_breakdown(self):
|
|
"""Test market liquidity quarterly breakdown."""
|
|
client = GovmapClient()
|
|
|
|
# Deals spread across specific quarters
|
|
deals = [
|
|
{"dealDate": "2023-01-15"}, # Q1
|
|
{"dealDate": "2023-02-20"}, # Q1
|
|
{"dealDate": "2023-05-10"}, # Q2
|
|
{"dealDate": "2023-08-05"}, # Q3
|
|
{"dealDate": "2023-11-12"}, # Q4
|
|
]
|
|
|
|
result = client.get_market_liquidity(deals)
|
|
|
|
assert "2023-Q1" in result["quarterly_breakdown"]
|
|
assert "2023-Q2" in result["quarterly_breakdown"]
|
|
assert "2023-Q3" in result["quarterly_breakdown"]
|
|
assert "2023-Q4" in result["quarterly_breakdown"]
|
|
assert result["quarterly_breakdown"]["2023-Q1"] == 2
|
|
|
|
def test_filter_deals_by_criteria_property_type(self):
|
|
"""Test filtering deals by property type."""
|
|
client = GovmapClient()
|
|
|
|
deals = [
|
|
{"assetTypeHeb": "דירה", "roomsNum": 3, "dealAmount": 1000000},
|
|
{"assetTypeHeb": "בית", "roomsNum": 5, "dealAmount": 2000000},
|
|
{"assetTypeHeb": "דירה", "roomsNum": 4, "dealAmount": 1500000},
|
|
]
|
|
|
|
filtered = client.filter_deals_by_criteria(deals, property_type="דירה")
|
|
|
|
assert len(filtered) == 2
|
|
assert all(d["assetTypeHeb"] == "דירה" for d in filtered)
|
|
|
|
def test_filter_deals_by_criteria_rooms(self):
|
|
"""Test filtering deals by room count."""
|
|
client = GovmapClient()
|
|
|
|
deals = [
|
|
{"assetRoomNum": 2, "dealAmount": 800000},
|
|
{"assetRoomNum": 3, "dealAmount": 1000000},
|
|
{"assetRoomNum": 4, "dealAmount": 1500000},
|
|
{"assetRoomNum": 5, "dealAmount": 2000000},
|
|
]
|
|
|
|
filtered = client.filter_deals_by_criteria(deals, min_rooms=3, max_rooms=4)
|
|
|
|
assert len(filtered) == 2
|
|
assert all(3 <= d["assetRoomNum"] <= 4 for d in filtered)
|
|
|
|
def test_filter_deals_by_criteria_price_range(self):
|
|
"""Test filtering deals by price range."""
|
|
client = GovmapClient()
|
|
|
|
deals = [
|
|
{"dealAmount": 800000},
|
|
{"dealAmount": 1000000},
|
|
{"dealAmount": 1500000},
|
|
{"dealAmount": 2000000},
|
|
]
|
|
|
|
filtered = client.filter_deals_by_criteria(deals, min_price=900000, max_price=1600000)
|
|
|
|
assert len(filtered) == 2
|
|
assert all(900000 <= d["dealAmount"] <= 1600000 for d in filtered)
|
|
|
|
def test_calculate_deal_statistics_success(self):
|
|
"""Test successful deal statistics calculation."""
|
|
client = GovmapClient()
|
|
|
|
deals = [
|
|
{"dealAmount": 1000000, "assetArea": 80, "price_per_sqm": 12500, "assetRoomNum": 3},
|
|
{"dealAmount": 1200000, "assetArea": 90, "price_per_sqm": 13333, "assetRoomNum": 4},
|
|
{"dealAmount": 900000, "assetArea": 70, "price_per_sqm": 12857, "assetRoomNum": 3},
|
|
]
|
|
|
|
stats = client.calculate_deal_statistics(deals)
|
|
|
|
assert "count" in stats
|
|
assert "price_stats" in stats
|
|
assert "area_stats" in stats
|
|
assert "price_per_sqm_stats" in stats
|
|
assert stats["count"] == 3
|
|
assert stats["price_stats"]["mean"] > 0
|
|
assert stats["area_stats"]["mean"] == pytest.approx(80.0)
|