Ruff fixes

This commit is contained in:
Nitzan Pomerantz
2025-10-30 22:24:40 +02:00
parent a80b38047c
commit e4aa6487ff
46 changed files with 1563 additions and 1062 deletions
+1 -1
View File
@@ -1 +1 @@
# Tests package for nadlan_mcp
# Tests package for nadlan_mcp
+20 -18
View File
@@ -8,8 +8,9 @@ IMPORTANT: These make real API calls and take longer to run.
"""
import pytest
from nadlan_mcp.govmap import GovmapClient
from nadlan_mcp.govmap.models import Deal, AutocompleteResponse
from nadlan_mcp.govmap.models import AutocompleteResponse, Deal
@pytest.fixture
@@ -37,16 +38,16 @@ class TestAutocompleteAPIHealth:
response = client.autocomplete_address("דיזנגוף תל אביב")
# Check response model fields exist
assert hasattr(response, 'results_count')
assert hasattr(response, 'results')
assert hasattr(response, "results_count")
assert hasattr(response, "results")
# Check result fields
if len(response.results) > 0:
result = response.results[0]
assert hasattr(result, 'id')
assert hasattr(result, 'text')
assert hasattr(result, 'type')
assert hasattr(result, 'coordinates')
assert hasattr(result, "id")
assert hasattr(result, "text")
assert hasattr(result, "type")
assert hasattr(result, "coordinates")
@pytest.mark.api_health
def test_autocomplete_coordinates_present(self, client):
@@ -128,18 +129,18 @@ class TestDealsAPIHealth:
deal = deals[0]
# Check required fields
assert hasattr(deal, 'objectid')
assert hasattr(deal, 'deal_amount')
assert hasattr(deal, 'deal_date')
assert hasattr(deal, "objectid")
assert hasattr(deal, "deal_amount")
assert hasattr(deal, "deal_date")
# Check common optional fields
assert hasattr(deal, 'asset_area')
assert hasattr(deal, 'property_type_description')
assert hasattr(deal, 'rooms')
assert hasattr(deal, 'floor')
assert hasattr(deal, "asset_area")
assert hasattr(deal, "property_type_description")
assert hasattr(deal, "rooms")
assert hasattr(deal, "floor")
# Check computed field
assert hasattr(deal, 'price_per_sqm')
assert hasattr(deal, "price_per_sqm")
class TestAPIDataQuality:
@@ -169,8 +170,9 @@ class TestAPIDataQuality:
# Check deal amounts are reasonable (10K to 100M NIS)
for deal in deals:
if deal.deal_amount > 0:
assert 10000 <= deal.deal_amount <= 100000000, \
f"Deal amount {deal.deal_amount} outside reasonable range"
assert (
10000 <= deal.deal_amount <= 100000000
), f"Deal amount {deal.deal_amount} outside reasonable range"
@pytest.mark.api_health
def test_dates_are_recent(self, client):
@@ -196,7 +198,7 @@ class TestAPIDataQuality:
pytest.skip("No deals")
# At least some deals should be from last 5 years
cutoff = date.today() - timedelta(days=5*365)
cutoff = date.today() - timedelta(days=5 * 365)
recent_deals = [d for d in deals if isinstance(d.deal_date, date) and d.deal_date >= cutoff]
assert len(recent_deals) > 0, "No recent deals found (within last 5 years)"
+9 -7
View File
@@ -2,8 +2,10 @@
Pytest configuration for nadlan_mcp tests.
"""
import pytest
from unittest.mock import Mock
import pytest
from tests.vcr_config import my_vcr
@@ -27,9 +29,9 @@ def sample_autocomplete_response():
"type": "address",
"score": 100,
"shape": "POINT(3870000.123 3770000.456)",
"data": {}
"data": {},
}
]
],
}
@@ -46,7 +48,7 @@ def sample_deals_response():
"assetArea": 100,
"settlementNameHeb": "תל אביב-יפו",
"propertyTypeDescription": "דירה",
"neighborhood": "test neighborhood"
"neighborhood": "test neighborhood",
},
{
"objectid": 456,
@@ -55,9 +57,9 @@ def sample_deals_response():
"assetArea": 120,
"settlementNameHeb": "תל אביב-יפو",
"propertyTypeDescription": "דירה",
"neighborhood": "test neighborhood"
}
]
"neighborhood": "test neighborhood",
},
],
}
+5 -5
View File
@@ -6,13 +6,16 @@ For comprehensive E2E testing, see test_mcp_tools_comprehensive.py
Target: Complete in <30 seconds
"""
import json
import pytest
from nadlan_mcp.fastmcp_server import (
autocomplete_address,
find_recent_deals_for_address,
get_street_deals,
get_deals_by_radius,
get_street_deals,
)
@@ -58,10 +61,7 @@ class TestMCPToolsSmokeTests:
"""Smoke test: Main tool works with minimal data."""
# Use very small limits to speed up
result = find_recent_deals_for_address(
self.TEST_ADDRESS,
years_back=1,
radius_meters=30,
max_deals=10
self.TEST_ADDRESS, years_back=1, radius_meters=30, max_deals=10
)
data = json.loads(result)
+12 -14
View File
@@ -6,19 +6,22 @@ of each MCP tool from end to end.
Mark as integration tests since they hit real APIs.
"""
import json
import pytest
from nadlan_mcp.fastmcp_server import (
autocomplete_address,
find_recent_deals_for_address,
analyze_market_trends,
get_valuation_comparables,
get_deal_statistics,
get_market_activity_metrics,
autocomplete_address,
compare_addresses,
get_street_deals,
get_neighborhood_deals,
find_recent_deals_for_address,
get_deal_statistics,
get_deals_by_radius,
get_market_activity_metrics,
get_neighborhood_deals,
get_street_deals,
get_valuation_comparables,
)
@@ -69,9 +72,7 @@ class TestMCPToolsE2E:
def test_analyze_market_trends(self):
"""Test market trend analysis."""
result = analyze_market_trends(
self.TEST_ADDRESS_1, years_back=3, radius_meters=100
)
result = analyze_market_trends(self.TEST_ADDRESS_1, years_back=3, radius_meters=100)
data = json.loads(result)
# Check response structure
@@ -83,10 +84,7 @@ class TestMCPToolsE2E:
def test_get_valuation_comparables(self):
"""Test getting valuation comparables."""
result = get_valuation_comparables(
self.TEST_ADDRESS_1,
years_back=3,
min_rooms=3.0,
max_rooms=5.0
self.TEST_ADDRESS_1, years_back=3, min_rooms=3.0, max_rooms=5.0
)
data = json.loads(result)
+1 -1
View File
@@ -32,4 +32,4 @@
},
"shape": "POINT(3871143.6159681133 3766329.319199102)"
}
]
]
+1 -1
View File
@@ -329,4 +329,4 @@
"polygonId": "52507812",
"price_per_sqm": 27230.77
}
]
]
+1 -1
View File
@@ -79,4 +79,4 @@
"polygon_id": "7172-7",
"objectid": 14315
}
]
]
+1 -1
View File
@@ -329,4 +329,4 @@
"polygonId": "52385050",
"price_per_sqm": 10238.1
}
]
]
+43 -32
View File
@@ -5,6 +5,7 @@ Comprehensive tests for filter_deals_by_criteria function.
"""
import pytest
from nadlan_mcp.govmap.filters import filter_deals_by_criteria
from nadlan_mcp.govmap.models import Deal, DealFilters
@@ -80,83 +81,79 @@ class TestFilterDealsByCriteria:
result = filter_deals_by_criteria(sample_deals, property_type="דירה")
# Should match "דירה" and "דירת גג" via substring match.
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 4}
assert {d.objectid for d in result} == {1, 2, 4}
def test_filter_by_min_rooms(self, sample_deals):
"""Test filtering by minimum rooms."""
result = filter_deals_by_criteria(sample_deals, min_rooms=4.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
assert {d.objectid for d in result} == {2, 3, 5}
def test_filter_by_max_rooms(self, sample_deals):
"""Test filtering by maximum rooms."""
result = filter_deals_by_criteria(sample_deals, max_rooms=3.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
assert {d.objectid for d in result} == {1, 4}
def test_filter_by_room_range(self, sample_deals):
"""Test filtering by room range."""
result = filter_deals_by_criteria(sample_deals, min_rooms=3.0, max_rooms=4.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
assert {d.objectid for d in result} == {1, 2}
def test_filter_by_min_price(self, sample_deals):
"""Test filtering by minimum price."""
result = filter_deals_by_criteria(sample_deals, min_price=1200000.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
assert {d.objectid for d in result} == {2, 3, 5}
def test_filter_by_max_price(self, sample_deals):
"""Test filtering by maximum price."""
result = filter_deals_by_criteria(sample_deals, max_price=1000000.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
assert {d.objectid for d in result} == {1, 4}
def test_filter_by_price_range(self, sample_deals):
"""Test filtering by price range."""
result = filter_deals_by_criteria(
sample_deals, min_price=1000000.0, max_price=1500000.0
)
result = filter_deals_by_criteria(sample_deals, min_price=1000000.0, max_price=1500000.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
assert {d.objectid for d in result} == {1, 2, 5}
def test_filter_by_min_area(self, sample_deals):
"""Test filtering by minimum area."""
result = filter_deals_by_criteria(sample_deals, min_area=100.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
assert {d.objectid for d in result} == {2, 3, 5}
def test_filter_by_max_area(self, sample_deals):
"""Test filtering by maximum area."""
result = filter_deals_by_criteria(sample_deals, max_area=80.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
assert {d.objectid for d in result} == {1, 4}
def test_filter_by_area_range(self, sample_deals):
"""Test filtering by area range."""
result = filter_deals_by_criteria(
sample_deals, min_area=80.0, max_area=120.0
)
result = filter_deals_by_criteria(sample_deals, min_area=80.0, max_area=120.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
assert {d.objectid for d in result} == {1, 2, 5}
def test_filter_by_min_floor(self, sample_deals):
"""Test filtering by minimum floor."""
result = filter_deals_by_criteria(sample_deals, min_floor=2)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
assert {d.objectid for d in result} == {1, 2, 5}
def test_filter_by_max_floor(self, sample_deals):
"""Test filtering by maximum floor."""
result = filter_deals_by_criteria(sample_deals, max_floor=2)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 3, 4}
assert {d.objectid for d in result} == {1, 3, 4}
def test_filter_by_floor_range(self, sample_deals):
"""Test filtering by floor range."""
result = filter_deals_by_criteria(sample_deals, min_floor=1, max_floor=5)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 4}
assert {d.objectid for d in result} == {1, 2, 4}
def test_combined_filters(self, sample_deals):
"""Test combining multiple filters."""
@@ -171,19 +168,17 @@ class TestFilterDealsByCriteria:
# objectid=1: "דירה", 3 rooms, 1M price ✓
# objectid=2: "דירת גג" (matches "דירה" variant), 4 rooms, 1.5M price ✓
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
assert {d.objectid for d in result} == {1, 2}
def test_filter_using_dealfilters_model(self, sample_deals):
"""Test filtering using DealFilters model."""
filters = DealFilters(
property_type="דירה", min_rooms=3.0, max_rooms=4.0
)
filters = DealFilters(property_type="דירה", min_rooms=3.0, max_rooms=4.0)
result = filter_deals_by_criteria(sample_deals, filters=filters)
# Should match objectid=1 and objectid=2
# objectid=1: "דירה", 3 rooms ✓
# objectid=2: "דירת גג" (matches "דירה" variant), 4 rooms ✓
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
assert {d.objectid for d in result} == {1, 2}
def test_filter_using_dict(self, sample_deals):
"""Test filtering using dict (converted to DealFilters)."""
@@ -197,23 +192,35 @@ class TestFilterDealsByCriteria:
# objectid=1: "דירה", 3 rooms ✓
# objectid=2: "דירת גג" (matches "דירה" variant), 4 rooms ✓
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
assert {d.objectid for d in result} == {1, 2}
def test_individual_params_override_filters_model(self, sample_deals):
"""Test that individual parameters override filters model."""
filters = DealFilters(min_rooms=5.0) # Would match only objectid=3
result = filter_deals_by_criteria(
sample_deals, filters=filters, min_rooms=3.0 # Override to 3.0
sample_deals,
filters=filters,
min_rooms=3.0, # Override to 3.0
)
# Should use min_rooms=3.0, not 5.0
assert len(result) == 4
assert set(d.objectid for d in result) == {1, 2, 3, 5}
assert {d.objectid for d in result} == {1, 2, 3, 5}
def test_filters_exclude_deals_with_missing_property_type(self):
"""Test that deals with missing property_type are excluded when filter active."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", property_type_description=None),
Deal(
objectid=1,
deal_amount=1000000,
deal_date="2024-01-01",
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=1000000,
deal_date="2024-01-01",
property_type_description=None,
),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01"), # No property_type
]
result = filter_deals_by_criteria(deals, property_type="דירה")
@@ -281,13 +288,17 @@ class TestFilterDealsByCriteria:
"""Test floor filtering with Hebrew floor descriptions."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", floor="קרקע"), # Ground=0
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", floor="קומה 3"), # Floor 3
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01", floor="מרתף"), # Basement=-1
Deal(
objectid=2, deal_amount=1000000, deal_date="2024-01-01", floor="קומה 3"
), # Floor 3
Deal(
objectid=3, deal_amount=1000000, deal_date="2024-01-01", floor="מרתף"
), # Basement=-1
]
result = filter_deals_by_criteria(deals, min_floor=0)
# Should match ground (0) and floor 3, but not basement (-1)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
assert {d.objectid for d in result} == {1, 2}
@pytest.mark.parametrize(
"min_val,max_val,expected_count",
+179 -36
View File
@@ -4,15 +4,17 @@ Tests for nadlan_mcp.govmap.market_analysis module.
Comprehensive tests for market analysis functions.
"""
from datetime import datetime, timedelta
import pytest
from datetime import date, datetime, timedelta
from nadlan_mcp.govmap.market_analysis import (
parse_deal_dates,
calculate_market_activity_score,
analyze_investment_potential,
calculate_market_activity_score,
get_market_liquidity,
parse_deal_dates,
)
from nadlan_mcp.govmap.models import Deal, MarketActivityScore, InvestmentAnalysis, LiquidityMetrics
from nadlan_mcp.govmap.models import Deal, InvestmentAnalysis, LiquidityMetrics, MarketActivityScore
def get_recent_date(months_ago=0, days_ago=0):
@@ -27,11 +29,36 @@ class TestParseDealDates:
def sample_deals(self):
"""Create sample deals spanning multiple months (recent dates)."""
return [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=4, days_ago=15), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=4, days_ago=10), asset_area=85),
Deal(objectid=3, deal_amount=1200000, deal_date=get_recent_date(months_ago=3, days_ago=20), asset_area=90),
Deal(objectid=4, deal_amount=1300000, deal_date=get_recent_date(months_ago=2, days_ago=25), asset_area=95),
Deal(objectid=5, deal_amount=1400000, deal_date=get_recent_date(months_ago=1, days_ago=18), asset_area=100),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=4, days_ago=15),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=get_recent_date(months_ago=4, days_ago=10),
asset_area=85,
),
Deal(
objectid=3,
deal_amount=1200000,
deal_date=get_recent_date(months_ago=3, days_ago=20),
asset_area=90,
),
Deal(
objectid=4,
deal_amount=1300000,
deal_date=get_recent_date(months_ago=2, days_ago=25),
asset_area=95,
),
Deal(
objectid=5,
deal_amount=1400000,
deal_date=get_recent_date(months_ago=1, days_ago=18),
asset_area=100,
),
]
def test_parse_deal_dates_basic(self, sample_deals):
@@ -60,8 +87,18 @@ class TestParseDealDates:
"""Test parsing with date objects instead of strings."""
recent = datetime.now().date()
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=recent - timedelta(days=30), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=recent - timedelta(days=60), asset_area=85),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=recent - timedelta(days=30),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=recent - timedelta(days=60),
asset_area=85,
),
]
deal_dates, monthly, _ = parse_deal_dates(deals)
@@ -71,8 +108,18 @@ class TestParseDealDates:
def test_parse_deal_dates_all_valid(self):
"""Test that all valid dates are parsed."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=2), asset_area=85),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=1),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=get_recent_date(months_ago=2),
asset_area=85,
),
]
deal_dates, _, _ = parse_deal_dates(deals)
assert len(deal_dates) == 2
@@ -90,7 +137,12 @@ class TestCalculateMarketActivityScore:
"""Test basic market activity calculation."""
# Create 12 deals spread across last 12 months
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=i),
asset_area=80,
)
for i in range(12)
]
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -106,10 +158,15 @@ class TestCalculateMarketActivityScore:
# Create 120 deals spread across last 10 months = ~12 deals/month (very high)
deals = []
for i in range(120):
month_ago = (i % 10)
month_ago = i % 10
day = (i % 28) + 1
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=month_ago, days_ago=day),
asset_area=80,
)
)
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -118,8 +175,18 @@ class TestCalculateMarketActivityScore:
def test_market_activity_low_volume(self):
"""Test activity score with low volume."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=6), asset_area=85),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=1),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=get_recent_date(months_ago=6),
asset_area=85,
),
]
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -135,7 +202,12 @@ class TestCalculateMarketActivityScore:
num_deals = i + 1 # Increasing: 1 deal earliest, 12 deals most recent
for j in range(num_deals):
deals.append(
Deal(objectid=len(deals), deal_amount=1000000, deal_date=get_recent_date(months_ago=months_ago, days_ago=j), asset_area=80)
Deal(
objectid=len(deals),
deal_amount=1000000,
deal_date=get_recent_date(months_ago=months_ago, days_ago=j),
asset_area=80,
)
)
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -150,7 +222,12 @@ class TestCalculateMarketActivityScore:
num_deals = 12 - i # Decreasing: 12 deals earliest, 1 deal most recent
for j in range(num_deals):
deals.append(
Deal(objectid=len(deals), deal_amount=1000000, deal_date=get_recent_date(months_ago=months_ago, days_ago=j), asset_area=80)
Deal(
objectid=len(deals),
deal_amount=1000000,
deal_date=get_recent_date(months_ago=months_ago, days_ago=j),
asset_area=80,
)
)
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -160,7 +237,12 @@ class TestCalculateMarketActivityScore:
"""Test trend detection - stable activity."""
# Same number of deals each month
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=i),
asset_area=80,
)
for i in range(12)
]
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -171,8 +253,18 @@ class TestCalculateMarketActivityScore:
def test_market_activity_insufficient_data_for_trend(self):
"""Test trend with insufficient data."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=2), asset_area=85),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=1),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=get_recent_date(months_ago=2),
asset_area=85,
),
]
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -198,7 +290,12 @@ class TestCalculateMarketActivityScore:
month_ago = i % months
day = (i // months) % 28
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=month_ago, days_ago=day),
asset_area=80,
)
)
score = calculate_market_activity_score(deals, time_period_months=12)
@@ -212,9 +309,15 @@ class TestAnalyzeInvestmentPotential:
"""Test basic investment analysis."""
# Create deals with increasing prices
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100), # 10000/sqm
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100), # 11000/sqm
Deal(objectid=3, deal_amount=1200000, deal_date="2024-03-01", asset_area=100), # 12000/sqm
Deal(
objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100
), # 10000/sqm
Deal(
objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100
), # 11000/sqm
Deal(
objectid=3, deal_amount=1200000, deal_date="2024-03-01", asset_area=100
), # 12000/sqm
]
analysis = analyze_investment_potential(deals)
@@ -253,7 +356,12 @@ class TestAnalyzeInvestmentPotential:
"""Test volatility calculation with low volatility."""
# Prices very similar
deals = [
Deal(objectid=i, deal_amount=1000000 + i*1000, deal_date=f"2024-{i+1:02d}-01", asset_area=100)
Deal(
objectid=i,
deal_amount=1000000 + i * 1000,
deal_date=f"2024-{i + 1:02d}-01",
asset_area=100,
)
for i in range(5)
]
analysis = analyze_investment_potential(deals)
@@ -278,7 +386,12 @@ class TestAnalyzeInvestmentPotential:
def test_investment_analysis_data_quality_excellent(self):
"""Test data quality assessment with excellent data."""
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=f"2024-{(i%12)+1:02d}-01", asset_area=100)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=f"2024-{(i % 12) + 1:02d}-01",
asset_area=100,
)
for i in range(25)
]
analysis = analyze_investment_potential(deals)
@@ -334,7 +447,7 @@ class TestAnalyzeInvestmentPotential:
def test_investment_analysis_price_trends(self, price_changes, expected_trend):
"""Parametrized test for price trend detection."""
deals = [
Deal(objectid=i, deal_amount=price, deal_date=f"2024-{i+1:02d}-01", asset_area=100)
Deal(objectid=i, deal_amount=price, deal_date=f"2024-{i + 1:02d}-01", asset_area=100)
for i, price in enumerate(price_changes)
]
analysis = analyze_investment_potential(deals)
@@ -348,7 +461,12 @@ class TestGetMarketLiquidity:
def test_market_liquidity_basic(self):
"""Test basic liquidity calculation."""
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=i),
asset_area=80,
)
for i in range(12)
]
liquidity = get_market_liquidity(deals, time_period_months=12)
@@ -367,7 +485,12 @@ class TestGetMarketLiquidity:
month_ago = i % 10
day = (i % 28) + 1
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=month_ago, days_ago=day),
asset_area=80,
)
)
liquidity = get_market_liquidity(deals, time_period_months=12)
@@ -377,8 +500,18 @@ class TestGetMarketLiquidity:
def test_market_liquidity_low(self):
"""Test low liquidity."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=6), asset_area=85),
Deal(
objectid=1,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=1),
asset_area=80,
),
Deal(
objectid=2,
deal_amount=1100000,
deal_date=get_recent_date(months_ago=6),
asset_area=85,
),
]
liquidity = get_market_liquidity(deals, time_period_months=12)
@@ -389,7 +522,12 @@ class TestGetMarketLiquidity:
"""Test deal velocity calculation."""
# 12 deals spread evenly across 12 months
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=i),
asset_area=80,
)
for i in range(12)
]
liquidity = get_market_liquidity(deals, time_period_months=12)
@@ -420,7 +558,12 @@ class TestGetMarketLiquidity:
month_ago = i % months
day = (i // months) % 28
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
Deal(
objectid=i,
deal_amount=1000000,
deal_date=get_recent_date(months_ago=month_ago, days_ago=day),
asset_area=80,
)
)
liquidity = get_market_liquidity(deals, time_period_months=12)
+73 -126
View File
@@ -5,21 +5,20 @@ This module tests all Pydantic model validation, serialization,
and computed fields to ensure type safety and correctness.
"""
import pytest
from datetime import datetime
from pydantic import ValidationError
import pytest
from nadlan_mcp.govmap.models import (
CoordinatePoint,
Address,
AutocompleteResult,
AutocompleteResponse,
AutocompleteResult,
CoordinatePoint,
Deal,
DealFilters,
DealStatistics,
MarketActivityScore,
InvestmentAnalysis,
LiquidityMetrics,
DealFilters,
MarketActivityScore,
)
@@ -51,11 +50,7 @@ class TestAddress:
"""Test creating valid address."""
coord = CoordinatePoint(longitude=180000.0, latitude=650000.0)
address = Address(
text="סוקולוב 38 חולון",
id="addr123",
type="address",
score=95.5,
coordinates=coord
text="סוקולוב 38 חולון", id="addr123", type="address", score=95.5, coordinates=coord
)
assert address.text == "סוקולוב 38 חולון"
assert address.score == 95.5
@@ -63,11 +58,7 @@ class TestAddress:
def test_address_without_coordinates(self):
"""Test creating address without coordinates."""
address = Address(
text="סוקולוב 38 חולון",
id="addr123",
type="address"
)
address = Address(text="סוקולוב 38 חולון", id="addr123", type="address")
assert address.coordinates is None
assert address.score == 0 # Default value
@@ -84,7 +75,7 @@ class TestAutocompleteResult:
type="city",
score=100.0,
coordinates=coord,
shape="POINT(180000.0 650000.0)"
shape="POINT(180000.0 650000.0)",
)
assert result.text == "חולון"
assert result.coordinates.longitude == 180000.0
@@ -92,11 +83,7 @@ class TestAutocompleteResult:
def test_result_without_shape(self):
"""Test result without shape data."""
result = AutocompleteResult(
text="חולון",
id="city123",
type="city"
)
result = AutocompleteResult(text="חולון", id="city123", type="city")
assert result.shape is None
assert result.coordinates is None
@@ -108,7 +95,7 @@ class TestAutocompleteResponse:
"""Test creating valid autocomplete response."""
results = [
AutocompleteResult(text="חולון", id="city1", type="city"),
AutocompleteResult(text="חולון סוקולוב", id="street1", type="street")
AutocompleteResult(text="חולון סוקולוב", id="street1", type="street"),
]
response = AutocompleteResponse(resultsCount=2, results=results)
assert response.results_count == 2
@@ -116,10 +103,7 @@ class TestAutocompleteResponse:
def test_response_with_alias(self):
"""Test that camelCase alias works."""
response = AutocompleteResponse.model_validate({
"resultsCount": 5,
"results": []
})
response = AutocompleteResponse.model_validate({"resultsCount": 5, "results": []})
assert response.results_count == 5
def test_empty_response(self):
@@ -143,7 +127,7 @@ class TestDeal:
property_type_description="דירה",
street_name="סוקולוב",
house_number="38",
rooms=3.5
rooms=3.5,
)
assert deal.objectid == 12345
assert deal.deal_amount == 1500000.0
@@ -151,43 +135,31 @@ class TestDeal:
def test_deal_with_aliases(self):
"""Test creating deal using API camelCase field names."""
deal = Deal.model_validate({
"objectid": 12345,
"dealAmount": 1500000.0,
"dealDate": "2024-01-15",
"assetArea": 85.0,
"propertyTypeDescription": "דירה"
})
deal = Deal.model_validate(
{
"objectid": 12345,
"dealAmount": 1500000.0,
"dealDate": "2024-01-15",
"assetArea": 85.0,
"propertyTypeDescription": "דירה",
}
)
assert deal.deal_amount == 1500000.0
assert deal.property_type_description == "דירה"
def test_deal_price_per_sqm_computed(self):
"""Test price_per_sqm computed field."""
deal = Deal(
objectid=12345,
deal_amount=1500000.0,
deal_date="2024-01-15",
asset_area=85.0
)
deal = Deal(objectid=12345, deal_amount=1500000.0, deal_date="2024-01-15", asset_area=85.0)
assert deal.price_per_sqm == round(1500000.0 / 85.0, 2)
def test_deal_price_per_sqm_no_area(self):
"""Test price_per_sqm returns None when area is missing."""
deal = Deal(
objectid=12345,
deal_amount=1500000.0,
deal_date="2024-01-15"
)
deal = Deal(objectid=12345, deal_amount=1500000.0, deal_date="2024-01-15")
assert deal.price_per_sqm is None
def test_deal_price_per_sqm_zero_area(self):
"""Test price_per_sqm returns None when area is zero."""
deal = Deal(
objectid=12345,
deal_amount=1500000.0,
deal_date="2024-01-15",
asset_area=0.0
)
deal = Deal(objectid=12345, deal_amount=1500000.0, deal_date="2024-01-15", asset_area=0.0)
assert deal.price_per_sqm is None
def test_deal_extra_fields_allowed(self):
@@ -197,7 +169,7 @@ class TestDeal:
"dealAmount": 1500000.0,
"dealDate": "2024-01-15",
"extra_field": "extra_value",
"another_field": 123
"another_field": 123,
}
deal = Deal.model_validate(deal_data)
# Extra fields should be stored
@@ -205,12 +177,7 @@ class TestDeal:
def test_deal_serialization(self):
"""Test deal serialization to dict."""
deal = Deal(
objectid=12345,
deal_amount=1500000.0,
deal_date="2024-01-15",
asset_area=85.0
)
deal = Deal(objectid=12345, deal_amount=1500000.0, deal_date="2024-01-15", asset_area=85.0)
deal_dict = deal.model_dump()
assert deal_dict["objectid"] == 12345
assert deal_dict["deal_amount"] == 1500000.0
@@ -218,11 +185,7 @@ class TestDeal:
def test_deal_serialization_exclude_none(self):
"""Test deal serialization excluding None values."""
deal = Deal(
objectid=12345,
deal_amount=1500000.0,
deal_date="2024-01-15"
)
deal = Deal(objectid=12345, deal_amount=1500000.0, deal_date="2024-01-15")
deal_dict = deal.model_dump(exclude_none=True)
assert "asset_area" not in deal_dict
assert "rooms" not in deal_dict
@@ -240,28 +203,11 @@ class TestDealStatistics:
"""Test creating valid deal statistics."""
stats = DealStatistics(
total_deals=100,
price_statistics={
"mean": 1500000.0,
"median": 1400000.0,
"std_dev": 200000.0
},
area_statistics={
"mean": 85.5,
"median": 82.0
},
price_per_sqm_statistics={
"mean": 17500.0,
"median": 17200.0
},
property_type_distribution={
"דירה": 80,
"דירת גן": 15,
"פנטהאוז": 5
},
date_range={
"earliest": "2022-01-01",
"latest": "2024-12-31"
}
price_statistics={"mean": 1500000.0, "median": 1400000.0, "std_dev": 200000.0},
area_statistics={"mean": 85.5, "median": 82.0},
price_per_sqm_statistics={"mean": 17500.0, "median": 17200.0},
property_type_distribution={"דירה": 80, "דירת גן": 15, "פנטהאוז": 5},
date_range={"earliest": "2022-01-01", "latest": "2024-12-31"},
)
assert stats.total_deals == 100
assert stats.price_statistics["mean"] == 1500000.0
@@ -286,7 +232,7 @@ class TestMarketActivityScore:
deals_per_month=10.0,
trend="increasing",
time_period_months=12,
monthly_distribution={"2024-01": 8, "2024-02": 12}
monthly_distribution={"2024-01": 8, "2024-02": 12},
)
assert score.activity_score == 75.5
assert score.trend == "increasing"
@@ -300,7 +246,7 @@ class TestMarketActivityScore:
total_deals=100,
deals_per_month=8.0,
trend="stable",
time_period_months=12
time_period_months=12,
)
with pytest.raises(ValidationError):
@@ -309,7 +255,7 @@ class TestMarketActivityScore:
total_deals=100,
deals_per_month=8.0,
trend="stable",
time_period_months=12
time_period_months=12,
)
@@ -327,7 +273,7 @@ class TestInvestmentAnalysis:
avg_price_per_sqm=17500.0,
price_change_pct=12.5,
total_deals=85,
data_quality="good"
data_quality="good",
)
assert analysis.investment_score == 68.5
assert analysis.price_trend == "increasing"
@@ -345,7 +291,7 @@ class TestInvestmentAnalysis:
avg_price_per_sqm=17000.0,
price_change_pct=5.0,
total_deals=100,
data_quality="excellent"
data_quality="excellent",
)
@@ -360,7 +306,7 @@ class TestLiquidityMetrics:
time_period_months=12,
avg_deals_per_month=12.5,
deal_velocity=12.5,
market_activity_level="high"
market_activity_level="high",
)
assert metrics.liquidity_score == 82.3
assert metrics.market_activity_level == "high"
@@ -381,7 +327,7 @@ class TestDealFilters:
min_area=60.0,
max_area=100.0,
min_floor=1,
max_floor=5
max_floor=5,
)
assert filters.property_type == "דירה"
assert filters.min_rooms == 2.0
@@ -425,45 +371,42 @@ class TestDealFilters:
class TestModelIntegration:
"""Integration tests for models working together."""
def test_deal_to_statistics_workflow(self):
"""Test creating deals and calculating statistics."""
# Import the function to test integration
from nadlan_mcp.govmap.statistics import calculate_deal_statistics
def test_deal_to_statistics_workflow(self):
"""Test creating deals and calculating statistics."""
# Import the function to test integration
from nadlan_mcp.govmap.statistics import calculate_deal_statistics
deals = [
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-01",
asset_area=100.0,
property_type_description="דירה"
),
Deal(
objectid=2,
deal_amount=2000000.0,
deal_date="2024-01-02",
asset_area=100.0,
property_type_description="דירה"
),
]
deals = [
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-01",
asset_area=100.0,
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=2000000.0,
deal_date="2024-01-02",
asset_area=100.0,
property_type_description="דירה",
),
]
stats = calculate_deal_statistics(deals)
stats = calculate_deal_statistics(deals)
assert isinstance(stats, DealStatistics)
assert stats.total_deals == 2
assert stats.price_statistics["mean"] == 1500000.0
assert stats.price_per_sqm_statistics["mean"] == 15000.0
assert stats.property_type_distribution["דירה"] == 2
assert isinstance(stats, DealStatistics)
assert stats.total_deals == 2
assert stats.price_statistics["mean"] == 1500000.0
assert stats.price_per_sqm_statistics["mean"] == 15000.0
assert stats.property_type_distribution["דירה"] == 2
def test_autocomplete_to_deals_workflow(self):
"""Test autocomplete response leading to deal search."""
# Simulate autocomplete response
coord = CoordinatePoint(longitude=180000.0, latitude=650000.0)
result = AutocompleteResult(
text="סוקולוב 38 חולון",
id="addr123",
type="address",
coordinates=coord
text="סוקולוב 38 חולון", id="addr123", type="address", coordinates=coord
)
response = AutocompleteResponse(resultsCount=1, results=[result])
@@ -478,14 +421,18 @@ class TestModelIntegration:
min_rooms=3.0,
max_rooms=4.0,
min_price=1000000.0,
max_price=2000000.0
max_price=2000000.0,
)
# Create test deals
deals = [
Deal(objectid=1, deal_amount=1200000.0, deal_date="2024-01-01", rooms=3.0),
Deal(objectid=2, deal_amount=2500000.0, deal_date="2024-01-02", rooms=4.0), # Price too high
Deal(objectid=3, deal_amount=1500000.0, deal_date="2024-01-03", rooms=2.0), # Too few rooms
Deal(
objectid=2, deal_amount=2500000.0, deal_date="2024-01-02", rooms=4.0
), # Price too high
Deal(
objectid=3, deal_amount=1500000.0, deal_date="2024-01-03", rooms=2.0
), # Too few rooms
]
# Manually check which deals would pass
+60 -13
View File
@@ -4,10 +4,12 @@ Tests for nadlan_mcp.govmap.statistics module.
Comprehensive tests for statistical calculation functions.
"""
import pytest
from datetime import date
from nadlan_mcp.govmap.statistics import calculate_deal_statistics, calculate_std_dev
import pytest
from nadlan_mcp.govmap.models import Deal, DealStatistics
from nadlan_mcp.govmap.statistics import calculate_deal_statistics, calculate_std_dev
class TestCalculateDealStatistics:
@@ -135,8 +137,12 @@ class TestCalculateDealStatistics:
"""Test statistics when some deals have zero prices."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=0.0, deal_date="2024-01-02", asset_area=100.0), # Zero price excluded
Deal(objectid=3, deal_amount=-1.0, deal_date="2024-01-03", asset_area=120.0), # Negative excluded
Deal(
objectid=2, deal_amount=0.0, deal_date="2024-01-02", asset_area=100.0
), # Zero price excluded
Deal(
objectid=3, deal_amount=-1.0, deal_date="2024-01-03", asset_area=120.0
), # Negative excluded
]
stats = calculate_deal_statistics(deals)
@@ -161,8 +167,18 @@ class TestCalculateDealStatistics:
def test_deals_with_missing_property_types(self):
"""Test statistics when some deals have missing property types."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", property_type_description=None),
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-01",
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=1500000.0,
deal_date="2024-01-02",
property_type_description=None,
),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03"), # No property_type
]
stats = calculate_deal_statistics(deals)
@@ -196,7 +212,13 @@ class TestCalculateDealStatistics:
def test_single_deal(self):
"""Test statistics with single deal."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0, property_type_description="דירה")
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-01",
asset_area=80.0,
property_type_description="דירה",
)
]
stats = calculate_deal_statistics(deals)
@@ -238,8 +260,18 @@ class TestCalculateDealStatistics:
def test_date_handling_with_iso_strings(self):
"""Test date range calculation with ISO format strings."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-15T00:00:00.000Z", asset_area=80.0),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-02-01T12:30:45.123Z", asset_area=100.0),
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-15T00:00:00.000Z",
asset_area=80.0,
),
Deal(
objectid=2,
deal_amount=1500000.0,
deal_date="2024-02-01T12:30:45.123Z",
asset_area=100.0,
),
]
stats = calculate_deal_statistics(deals)
@@ -250,9 +282,24 @@ class TestCalculateDealStatistics:
def test_multiple_same_property_types(self):
"""Test property type distribution with duplicates."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", property_type_description="דירה"),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03", property_type_description="דירה"),
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-01",
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=1500000.0,
deal_date="2024-01-02",
property_type_description="דירה",
),
Deal(
objectid=3,
deal_amount=2000000.0,
deal_date="2024-01-03",
property_type_description="דירה",
),
]
stats = calculate_deal_statistics(deals)
@@ -289,7 +336,7 @@ class TestCalculateDealStatistics:
Deal(
objectid=i,
deal_amount=1000000.0 if has_price else 0.0, # Use 0.0 instead of None
deal_date=f"2024-01-{i+1:02d}",
deal_date=f"2024-01-{i + 1:02d}",
asset_area=80.0 if has_area else None,
)
)
+1 -2
View File
@@ -4,11 +4,10 @@ Unit tests for utils module.
Tests helper utilities including distance calculation, address matching, and floor parsing.
"""
import pytest
from nadlan_mcp.govmap.utils import (
calculate_distance,
is_same_building,
extract_floor_number,
is_same_building,
)
+2 -1
View File
@@ -5,11 +5,12 @@ Tests input validation functions for addresses, coordinates, integers, and deal
"""
import pytest
from nadlan_mcp.govmap.validators import (
validate_address,
validate_coordinates,
validate_positive_int,
validate_deal_type,
validate_positive_int,
)
+66 -91
View File
@@ -8,19 +8,22 @@ Updated for Phase 4.1 - Pydantic models integration.
"""
import json
import pytest
from unittest.mock import Mock, patch
from unittest.mock import patch
from nadlan_mcp import fastmcp_server
from nadlan_mcp.govmap.models import (
Deal, AutocompleteResponse, AutocompleteResult, CoordinatePoint,
DealStatistics, MarketActivityScore, InvestmentAnalysis, LiquidityMetrics
AutocompleteResponse,
AutocompleteResult,
CoordinatePoint,
Deal,
DealStatistics,
)
class TestAutocompleteAddress:
"""Test autocomplete_address MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_autocomplete(self, mock_client):
"""Test successful address autocomplete with correct field mapping."""
# Now returns AutocompleteResponse model
@@ -33,7 +36,7 @@ class TestAutocompleteAddress:
type="address",
score=100,
coordinates=CoordinatePoint(longitude=180000.5, latitude=650000.3),
shape="POINT(180000.5 650000.3)"
shape="POINT(180000.5 650000.3)",
),
AutocompleteResult(
id="address|ADDR|124",
@@ -41,9 +44,9 @@ class TestAutocompleteAddress:
type="address",
score=95,
coordinates=CoordinatePoint(longitude=180010.2, latitude=650005.7),
shape="POINT(180010.2 650005.7)"
)
]
shape="POINT(180010.2 650005.7)",
),
],
)
result = fastmcp_server.autocomplete_address("דיזנגוף תל אביב")
@@ -56,20 +59,19 @@ class TestAutocompleteAddress:
assert parsed[0]["coordinates"]["longitude"] == 180000.5
assert parsed[0]["coordinates"]["latitude"] == 650000.3
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_autocomplete_no_results(self, mock_client):
"""Test autocomplete with no results."""
# Now returns AutocompleteResponse model
mock_client.autocomplete_address.return_value = AutocompleteResponse(
resultsCount=0,
results=[]
resultsCount=0, results=[]
)
result = fastmcp_server.autocomplete_address("nonexistent address")
# With empty results, returns a message string
assert "No addresses found" in result
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_autocomplete_invalid_coordinates(self, mock_client):
"""Test autocomplete with invalid/missing coordinate format."""
# Now returns AutocompleteResponse model with result that has no coordinates
@@ -82,9 +84,9 @@ class TestAutocompleteAddress:
type="address",
score=100,
coordinates=None, # No coordinates parsed
shape="INVALID_FORMAT"
shape="INVALID_FORMAT",
)
]
],
)
result = fastmcp_server.autocomplete_address("test")
@@ -93,7 +95,7 @@ class TestAutocompleteAddress:
assert len(parsed) == 1
assert parsed[0]["text"] == "דיזנגוף 50"
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_autocomplete_missing_shape(self, mock_client):
"""Test autocomplete with missing shape field."""
# Mock with AutocompleteResponse model
@@ -105,9 +107,9 @@ class TestAutocompleteAddress:
text="דיזנגוף 50",
type="address",
score=100,
coordinates=None # No coordinates
coordinates=None, # No coordinates
)
]
],
)
result = fastmcp_server.autocomplete_address("test")
@@ -115,7 +117,7 @@ class TestAutocompleteAddress:
# When coordinates are None, the field isn't included in the response
assert "coordinates" not in parsed[0]
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_autocomplete_error_handling(self, mock_client):
"""Test autocomplete error handling."""
mock_client.autocomplete_address.side_effect = Exception("API Error")
@@ -128,7 +130,7 @@ class TestAutocompleteAddress:
class TestGetDealsByRadius:
"""Test get_deals_by_radius MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_get_deals(self, mock_client):
"""Test successful polygon metadata retrieval."""
# Mock with polygon metadata dicts (not Deal objects)
@@ -139,7 +141,7 @@ class TestGetDealsByRadius:
"settlementNameHeb": "תל אביב-יפו",
"streetNameHeb": "דיזנגוף",
"houseNum": 50,
"polygon_id": "123-456"
"polygon_id": "123-456",
}
]
mock_client.get_deals_by_radius.return_value = mock_polygons
@@ -152,7 +154,7 @@ class TestGetDealsByRadius:
assert parsed["total_polygons"] == 1
mock_client.get_deals_by_radius.assert_called_once()
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_deals_no_results(self, mock_client):
"""Test polygon metadata retrieval with no results."""
mock_client.get_deals_by_radius.return_value = []
@@ -160,7 +162,7 @@ class TestGetDealsByRadius:
result = fastmcp_server.get_deals_by_radius(650000.0, 180000.0, 500)
assert "No polygons found" in result
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_deals_strips_bloat_fields(self, mock_client):
"""Test that polygon metadata is returned as-is."""
# Mock with polygon metadata dicts
@@ -169,7 +171,7 @@ class TestGetDealsByRadius:
"objectid": 123,
"dealscount": "10",
"polygon_id": "abc123",
"settlementNameHeb": "Tel Aviv"
"settlementNameHeb": "Tel Aviv",
}
]
mock_client.get_deals_by_radius.return_value = mock_polygons
@@ -182,7 +184,7 @@ class TestGetDealsByRadius:
assert polygon["polygon_id"] == "abc123"
assert polygon["dealscount"] == "10"
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_deals_error_handling(self, mock_client):
"""Test error handling for deal retrieval."""
mock_client.get_deals_by_radius.side_effect = ValueError("Invalid coordinates")
@@ -194,7 +196,7 @@ class TestGetDealsByRadius:
class TestFindRecentDealsForAddress:
"""Test find_recent_deals_for_address MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_find_deals(self, mock_client):
"""Test successful deal finding with statistics."""
# Mock with Deal models
@@ -204,21 +206,19 @@ class TestFindRecentDealsForAddress:
deal_amount=2000000,
deal_date="2023-01-01",
asset_area=80.0,
priority=0
priority=0,
),
Deal(
objectid=124,
deal_amount=1800000,
deal_date="2023-01-02",
asset_area=70.0,
priority=1
)
priority=1,
),
]
mock_client.find_recent_deals_for_address.return_value = mock_deals
result = fastmcp_server.find_recent_deals_for_address(
"דיזנגוף 50 תל אביב", 2, 100, 100
)
result = fastmcp_server.find_recent_deals_for_address("דיזנגוף 50 תל אביב", 2, 100, 100)
parsed = json.loads(result)
assert "search_parameters" in parsed
@@ -227,20 +227,18 @@ class TestFindRecentDealsForAddress:
assert len(parsed["deals"]) == 2
assert parsed["market_statistics"]["deal_breakdown"]["total_deals"] == 2
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_find_deals_no_results(self, mock_client):
"""Test deal finding with no results."""
mock_client.find_recent_deals_for_address.return_value = []
result = fastmcp_server.find_recent_deals_for_address(
"nonexistent address", 2, 100, 100
)
result = fastmcp_server.find_recent_deals_for_address("nonexistent address", 2, 100, 100)
# When no deals, returns a text message, not JSON
assert "No second hand (used) deals found" in result
assert "nonexistent address" in result
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_find_deals_strips_bloat(self, mock_client):
"""Test that bloat fields are stripped."""
# Mock with Deal models
@@ -250,14 +248,12 @@ class TestFindRecentDealsForAddress:
deal_amount=2000000,
deal_date="2023-01-01",
shape="MULTIPOLYGON(...)",
sourceorder=1
sourceorder=1,
)
]
mock_client.find_recent_deals_for_address.return_value = mock_deals
result = fastmcp_server.find_recent_deals_for_address(
"test address", 2, 100, 100
)
result = fastmcp_server.find_recent_deals_for_address("test address", 2, 100, 100)
parsed = json.loads(result)
assert "shape" not in parsed["deals"][0]
@@ -267,7 +263,7 @@ class TestFindRecentDealsForAddress:
class TestAnalyzeMarketTrends:
"""Test analyze_market_trends MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_market_analysis(self, mock_client):
"""Test successful market trend analysis."""
# Mock with Deal models
@@ -279,7 +275,7 @@ class TestAnalyzeMarketTrends:
asset_area=80.0,
property_type_description="דירה",
neighborhood="תל אביב",
priority=1
priority=1,
)
]
mock_client.find_recent_deals_for_address.return_value = mock_deals
@@ -292,7 +288,7 @@ class TestAnalyzeMarketTrends:
assert "yearly_trends" in parsed
assert "top_property_types" in parsed
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_market_analysis_no_data(self, mock_client):
"""Test market analysis with no data."""
mock_client.find_recent_deals_for_address.return_value = []
@@ -307,25 +303,23 @@ class TestAnalyzeMarketTrends:
class TestCompareAddresses:
"""Test compare_addresses MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_comparison(self, mock_client):
"""Test successful address comparison."""
# Mock different deals for each address
mock_client.find_recent_deals_for_address.side_effect = [
[{"dealAmount": 2000000, "assetArea": 80, "price_per_sqm": 25000}],
[{"dealAmount": 1500000, "assetArea": 60, "price_per_sqm": 25000}]
[{"dealAmount": 1500000, "assetArea": 60, "price_per_sqm": 25000}],
]
result = fastmcp_server.compare_addresses(
["דיזנגוף 50 תל אביב", "הרצל 1 חולון"]
)
result = fastmcp_server.compare_addresses(["דיזנגוף 50 תל אביב", "הרצל 1 חולון"])
parsed = json.loads(result)
assert "addresses_compared" in parsed
assert parsed["addresses_compared"] == 2
assert "all_results" in parsed
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_comparison_error_handling(self, mock_client):
"""Test error handling in address comparison."""
mock_client.find_recent_deals_for_address.side_effect = ValueError("Invalid address")
@@ -340,10 +334,10 @@ class TestCompareAddresses:
class TestGetValuationComparables:
"""Test get_valuation_comparables MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_get_comparables(self, mock_client):
"""Test successful comparable retrieval with filtering."""
from nadlan_mcp.govmap.models import DealStatistics
# Mock with Deal models
mock_deals = [
Deal(
@@ -352,24 +346,21 @@ class TestGetValuationComparables:
deal_date="2023-01-01",
asset_area=80.0,
rooms=3.0,
property_type_description="דירה"
property_type_description="דירה",
)
]
mock_stats = DealStatistics(
total_deals=1,
price_statistics={"mean": 2000000},
area_statistics={"mean": 80},
price_per_sqm_statistics={"mean": 25000}
price_per_sqm_statistics={"mean": 25000},
)
mock_client.find_recent_deals_for_address.return_value = mock_deals
mock_client.filter_deals_by_criteria.return_value = mock_deals
mock_client.calculate_deal_statistics.return_value = mock_stats
result = fastmcp_server.get_valuation_comparables(
"דיזנגוף 50 תל אביב",
property_type="דירה",
min_rooms=2,
max_rooms=4
"דיזנגוף 50 תל אביב", property_type="דירה", min_rooms=2, max_rooms=4
)
parsed = json.loads(result)
@@ -378,10 +369,10 @@ class TestGetValuationComparables:
assert "comparables" in parsed
assert parsed["filters_applied"]["property_type"] == "דירה"
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_comparables_strips_bloat(self, mock_client):
"""Test that bloat fields are stripped from comparables."""
from nadlan_mcp.govmap.models import DealStatistics
# Mock with Deal models
mock_deals = [
Deal(
@@ -390,13 +381,10 @@ class TestGetValuationComparables:
deal_date="2023-01-01",
shape="MULTIPOLYGON(...)",
sourceorder=1,
source_polygon_id="abc"
source_polygon_id="abc",
)
]
mock_stats = DealStatistics(
total_deals=1,
price_statistics={"mean": 2000000}
)
mock_stats = DealStatistics(total_deals=1, price_statistics={"mean": 2000000})
mock_client.find_recent_deals_for_address.return_value = mock_deals
mock_client.filter_deals_by_criteria.return_value = mock_deals
mock_client.calculate_deal_statistics.return_value = mock_stats
@@ -413,23 +401,18 @@ class TestGetValuationComparables:
class TestGetDealStatistics:
"""Test get_deal_statistics MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_statistics_calculation(self, mock_client):
"""Test successful statistics calculation."""
from nadlan_mcp.govmap.models import DealStatistics
# Mock with Deal models
mock_deals = [
Deal(objectid=1, deal_amount=2000000, deal_date="2023-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=1800000, deal_date="2023-01-02", asset_area=70.0)
Deal(objectid=2, deal_amount=1800000, deal_date="2023-01-02", asset_area=70.0),
]
mock_stats = DealStatistics(
total_deals=2,
price_statistics={
"mean": 1900000,
"median": 1900000,
"min": 1800000,
"max": 2000000
}
price_statistics={"mean": 1900000, "median": 1900000, "min": 1800000, "max": 2000000},
)
mock_client.find_recent_deals_for_address.return_value = mock_deals
mock_client.filter_deals_by_criteria.return_value = mock_deals
@@ -446,28 +429,24 @@ class TestGetDealStatistics:
class TestGetMarketActivityMetrics:
"""Test get_market_activity_metrics MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_activity_metrics(self, mock_client):
"""Test successful market activity calculation."""
mock_deals = [
{
"dealDate": "2024-01-15T00:00:00.000Z",
"dealAmount": 2000000,
"assetArea": 80
}
{"dealDate": "2024-01-15T00:00:00.000Z", "dealAmount": 2000000, "assetArea": 80}
]
mock_client.find_recent_deals_for_address.return_value = mock_deals
mock_client.calculate_market_activity_score.return_value = {
"activity_score": 75,
"activity_level": "high"
"activity_level": "high",
}
mock_client.get_market_liquidity.return_value = {
"velocity_score": 8.5,
"liquidity_rating": "high"
"liquidity_rating": "high",
}
mock_client.analyze_investment_potential.return_value = {
"investment_score": 80,
"recommendation": "positive"
"recommendation": "positive",
}
result = fastmcp_server.get_market_activity_metrics("test address")
@@ -481,13 +460,11 @@ class TestGetMarketActivityMetrics:
class TestGetStreetDeals:
"""Test get_street_deals MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_street_deals(self, mock_client):
"""Test successful street deal retrieval."""
# Mock with Deal models
mock_deals = [
Deal(objectid=123, deal_amount=2000000, deal_date="2023-01-01")
]
mock_deals = [Deal(objectid=123, deal_amount=2000000, deal_date="2023-01-01")]
mock_client.get_street_deals.return_value = mock_deals
result = fastmcp_server.get_street_deals("12345", 100)
@@ -500,13 +477,11 @@ class TestGetStreetDeals:
class TestGetNeighborhoodDeals:
"""Test get_neighborhood_deals MCP tool."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_successful_neighborhood_deals(self, mock_client):
"""Test successful neighborhood deal retrieval."""
# Mock with Deal models
mock_deals = [
Deal(objectid=123, deal_amount=2000000, deal_date="2023-01-01")
]
mock_deals = [Deal(objectid=123, deal_amount=2000000, deal_date="2023-01-01")]
mock_client.get_neighborhood_deals.return_value = mock_deals
result = fastmcp_server.get_neighborhood_deals("12345", 100)
+217 -115
View File
@@ -4,33 +4,34 @@ Tests for the GovmapClient class.
Updated for Phase 4.1 - Pydantic models integration.
"""
import pytest
import requests
from unittest.mock import Mock, patch
from nadlan_mcp.govmap import GovmapClient
from nadlan_mcp.govmap.models import Deal, AutocompleteResponse, AutocompleteResult, CoordinatePoint
import pytest
from nadlan_mcp.config import GovmapConfig
from nadlan_mcp.govmap import GovmapClient
from nadlan_mcp.govmap.models import AutocompleteResponse, AutocompleteResult, CoordinatePoint, Deal
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'
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/"
custom_config = GovmapConfig(base_url=custom_url)
client = GovmapClient(custom_config)
assert client.base_url == "https://custom-api.example.com/api"
@patch('requests.Session')
@patch("requests.Session")
def test_autocomplete_address_success(self, mock_session_class):
"""Test successful address autocomplete."""
# Mock response
@@ -44,9 +45,9 @@ class TestGovmapClient:
"type": "address",
"score": 100,
"shape": "POINT(3870000.123 3770000.456)",
"data": {}
"data": {},
}
]
],
}
mock_response.raise_for_status.return_value = None
@@ -65,8 +66,8 @@ class TestGovmapClient:
assert result.results[0].coordinates is not None
assert result.results[0].coordinates.longitude == 3870000.123
mock_session.post.assert_called_once()
@patch('requests.Session')
@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()
@@ -83,23 +84,23 @@ class TestGovmapClient:
assert isinstance(result, AutocompleteResponse)
assert result.results_count == 0
assert len(result.results) == 0
@patch('requests.Session')
@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()
@@ -113,20 +114,20 @@ class TestGovmapClient:
text="test address",
type="address",
shape="POINT(3870000.123 3770000.456)",
coordinates=CoordinatePoint(longitude=3870000.123, latitude=3770000.456)
coordinates=CoordinatePoint(longitude=3870000.123, latitude=3770000.456),
)
]
],
)
# 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=[]):
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')
@patch("requests.Session")
def test_get_deals_by_radius_success(self, mock_session_class):
"""Test successful polygon metadata retrieval by radius."""
mock_response = Mock()
@@ -138,7 +139,7 @@ class TestGovmapClient:
"settlementNameHeb": "תל אביב-יפו",
"streetNameHeb": "דיזנגוף",
"houseNum": 50,
"polygon_id": "123-456"
"polygon_id": "123-456",
}
]
mock_response.raise_for_status.return_value = None
@@ -156,8 +157,8 @@ class TestGovmapClient:
assert result[0]["objectid"] == 12345
assert result[0]["polygon_id"] == "123-456"
mock_session.get.assert_called_once()
@patch('requests.Session')
@patch("requests.Session")
def test_get_street_deals_success(self, mock_session_class):
"""Test successful street deals query."""
mock_response = Mock()
@@ -170,16 +171,16 @@ class TestGovmapClient:
"dealDate": "2025-01-01T00:00:00.000Z",
"assetArea": 100,
"settlementNameHeb": "תל אביב-יפו",
"propertyTypeDescription": "דירה"
"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")
@@ -190,8 +191,8 @@ class TestGovmapClient:
assert result[0].asset_area == 100
assert result[0].price_per_sqm == 10000.0 # Computed field
mock_session.get.assert_called_once()
@patch('requests.Session')
@patch("requests.Session")
def test_get_neighborhood_deals_success(self, mock_session_class):
"""Test successful neighborhood deals query."""
mock_response = Mock()
@@ -204,16 +205,16 @@ class TestGovmapClient:
"dealDate": "2025-01-15T00:00:00.000Z",
"assetArea": 120,
"settlementNameHeb": "תל אביב-יפו",
"propertyTypeDescription": "דירה"
"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")
@@ -224,14 +225,20 @@ class TestGovmapClient:
assert result[0].asset_area == 120
assert result[0].price_per_sqm == round(2000000 / 120, 2)
mock_session.get.assert_called_once()
@patch('nadlan_mcp.govmap.client.GovmapClient.get_neighborhood_deals')
@patch('nadlan_mcp.govmap.client.GovmapClient.get_street_deals')
@patch('nadlan_mcp.govmap.client.GovmapClient.get_deals_by_radius')
@patch('nadlan_mcp.govmap.client.GovmapClient.autocomplete_address')
def test_find_recent_deals_for_address_integration(self, mock_autocomplete, mock_radius, mock_street, mock_neighborhood):
@patch("nadlan_mcp.govmap.client.GovmapClient.get_neighborhood_deals")
@patch("nadlan_mcp.govmap.client.GovmapClient.get_street_deals")
@patch("nadlan_mcp.govmap.client.GovmapClient.get_deals_by_radius")
@patch("nadlan_mcp.govmap.client.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."""
from nadlan_mcp.govmap.models import CoordinatePoint, AutocompleteResult, AutocompleteResponse
from nadlan_mcp.govmap.models import (
AutocompleteResponse,
AutocompleteResult,
CoordinatePoint,
)
# Mock autocomplete response - now returns AutocompleteResponse model
mock_autocomplete.return_value = AutocompleteResponse(
@@ -242,14 +249,19 @@ class TestGovmapClient:
id="addr123",
type="address",
coordinates=CoordinatePoint(longitude=3870000.123, latitude=3770000.456),
shape="POINT(3870000.123 3770000.456)"
shape="POINT(3870000.123 3770000.456)",
)
]
],
)
# Mock radius response - now returns List[Dict] (polygon metadata)
mock_radius.return_value = [
{"objectid": 1, "dealscount": "10", "polygon_id": "123-456", "settlementNameHeb": "Tel Aviv"}
{
"objectid": 1,
"dealscount": "10",
"polygon_id": "123-456",
"settlementNameHeb": "Tel Aviv",
}
]
# Mock street deals response - now returns List[Deal]
@@ -259,7 +271,7 @@ class TestGovmapClient:
deal_amount=1000000,
deal_date="2025-01-01T00:00:00.000Z",
street_name="Test Street",
house_number="1"
house_number="1",
)
]
@@ -270,7 +282,7 @@ class TestGovmapClient:
deal_amount=2000000,
deal_date="2025-01-15T00:00:00.000Z",
street_name="Test Street",
house_number="2"
house_number="2",
)
]
@@ -284,25 +296,25 @@ class TestGovmapClient:
# Should be sorted by priority first (street=1 before neighborhood=2), then by date
# Priority is set dynamically by find_recent_deals_for_address
assert hasattr(result[0], 'priority')
assert hasattr(result[1], 'priority')
assert hasattr(result[0], "priority")
assert hasattr(result[1], "priority")
assert result[0].priority <= result[1].priority # Lower priority comes first
@patch('requests.Session')
@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()
@@ -316,12 +328,12 @@ class TestGovmapClient:
text="test address",
type="address",
shape="INVALID_FORMAT", # Invalid format
coordinates=None # No coordinates
coordinates=None, # No coordinates
)
]
],
)
with patch.object(client, 'autocomplete_address', return_value=mock_autocomplete_result):
with patch.object(client, "autocomplete_address", return_value=mock_autocomplete_result):
with pytest.raises(ValueError, match="No coordinates found"):
client.find_recent_deals_for_address("test", years_back=1)
@@ -332,18 +344,21 @@ class TestMarketAnalysisFunctions:
def test_calculate_market_activity_score_success(self):
"""Test successful market activity score calculation."""
from nadlan_mcp.govmap.models import MarketActivityScore
client = GovmapClient()
# Sample deals with dates - now using Deal models
deals = [
Deal(objectid=i, deal_date=date, deal_amount=amount)
for i, (date, amount) in enumerate([
("2023-01-15", 1000000),
("2023-01-20", 1100000),
("2023-02-10", 1200000),
("2023-03-05", 1150000),
("2023-04-12", 1250000),
])
for i, (date, amount) in enumerate(
[
("2023-01-15", 1000000),
("2023-01-20", 1100000),
("2023-02-10", 1200000),
("2023-03-05", 1150000),
("2023-04-12", 1250000),
]
)
]
result = client.calculate_market_activity_score(deals, time_period_months=None)
@@ -360,14 +375,18 @@ class TestMarketAnalysisFunctions:
"""Test market activity score with empty deals list."""
client = GovmapClient()
with pytest.raises(ValueError, match="Cannot calculate market activity from empty deals list"):
with pytest.raises(
ValueError, match="Cannot calculate market activity from empty deals list"
):
client.calculate_market_activity_score([])
def test_calculate_market_activity_score_with_time_filter(self):
"""Test market activity score with time period filtering."""
# Note: With Pydantic models, deal_date is required and validated
from datetime import datetime, timedelta
from nadlan_mcp.govmap.models import MarketActivityScore
client = GovmapClient()
# Create deals spanning several months using recent dates
@@ -376,7 +395,7 @@ class TestMarketAnalysisFunctions:
Deal(
objectid=i,
deal_date=(today - timedelta(days=30 * month)).strftime("%Y-%m-%d"),
deal_amount=1000000 + i * 10000
deal_amount=1000000 + i * 10000,
)
for i, month in enumerate([1, 1, 2, 3, 3, 3, 6, 11], 1) # All within last 12 months
]
@@ -392,7 +411,9 @@ class TestMarketAnalysisFunctions:
# Generate many deals across multiple months for trend analysis - now using Deal models
deals = [
Deal(objectid=i, deal_date=f"2023-{(i % 6) + 1:02d}-15", deal_amount=1000000 + i * 10000)
Deal(
objectid=i, deal_date=f"2023-{(i % 6) + 1:02d}-15", deal_amount=1000000 + i * 10000
)
for i in range(1, 31) # 30 deals spread across 6 months
]
@@ -405,31 +426,34 @@ class TestMarketAnalysisFunctions:
def test_analyze_investment_potential_success(self):
"""Test successful investment potential analysis."""
from nadlan_mcp.govmap.models import InvestmentAnalysis
client = GovmapClient()
# Sample deals with price appreciation - now using Deal models
# Note: price_per_sqm is computed automatically from deal_amount / asset_area
deals = [
Deal(objectid=i, deal_date=date, deal_amount=amount, asset_area=80.0)
for i, (date, amount) in enumerate([
("2022-01-15", 1000000),
("2022-06-10", 1050000),
("2023-01-05", 1100000),
("2023-06-12", 1150000),
])
for i, (date, amount) in enumerate(
[
("2022-01-15", 1000000),
("2022-06-10", 1050000),
("2023-01-05", 1100000),
("2023-06-12", 1150000),
]
)
]
result = client.analyze_investment_potential(deals)
# Now returns InvestmentAnalysis model
assert isinstance(result, InvestmentAnalysis)
assert hasattr(result, 'price_appreciation_rate')
assert hasattr(result, 'price_volatility')
assert hasattr(result, 'market_stability')
assert hasattr(result, 'price_trend')
assert hasattr(result, 'avg_price_per_sqm')
assert hasattr(result, 'investment_score')
assert hasattr(result, 'data_quality')
assert hasattr(result, "price_appreciation_rate")
assert hasattr(result, "price_volatility")
assert hasattr(result, "market_stability")
assert hasattr(result, "price_trend")
assert hasattr(result, "avg_price_per_sqm")
assert hasattr(result, "investment_score")
assert hasattr(result, "data_quality")
assert 0 <= result.investment_score <= 100
assert result.price_trend in ["increasing", "stable", "decreasing"]
@@ -437,7 +461,9 @@ class TestMarketAnalysisFunctions:
"""Test investment potential with empty deals list."""
client = GovmapClient()
with pytest.raises(ValueError, match="Cannot analyze investment potential from empty deals list"):
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):
@@ -459,7 +485,12 @@ class TestMarketAnalysisFunctions:
# Deals with consistent prices (very stable) - now using Deal models
deals = [
Deal(objectid=i, deal_date=f"2023-{i:02d}-15", deal_amount=1000000 + i * 1000, asset_area=80.0)
Deal(
objectid=i,
deal_date=f"2023-{i:02d}-15",
deal_amount=1000000 + i * 1000,
asset_area=80.0,
)
for i in range(1, 13) # 12 months, slight increase
]
@@ -472,19 +503,22 @@ class TestMarketAnalysisFunctions:
def test_get_market_liquidity_success(self):
"""Test successful market liquidity calculation."""
from nadlan_mcp.govmap.models import LiquidityMetrics
client = GovmapClient()
# Sample deals across multiple quarters - now using Deal models
deals = [
Deal(objectid=i, deal_date=date, deal_amount=amount)
for i, (date, amount) in enumerate([
("2023-01-15", 1000000),
("2023-02-20", 1100000),
("2023-05-10", 1200000),
("2023-06-05", 1150000),
("2023-09-12", 1250000),
("2023-10-18", 1300000),
])
for i, (date, amount) in enumerate(
[
("2023-01-15", 1000000),
("2023-02-20", 1100000),
("2023-05-10", 1200000),
("2023-06-05", 1150000),
("2023-09-12", 1250000),
("2023-10-18", 1300000),
]
)
]
result = client.get_market_liquidity(deals, time_period_months=None)
@@ -500,12 +534,15 @@ class TestMarketAnalysisFunctions:
"""Test market liquidity with empty deals list."""
client = GovmapClient()
with pytest.raises(ValueError, match="Cannot calculate market liquidity from empty deals list"):
with pytest.raises(
ValueError, match="Cannot calculate market liquidity from empty deals list"
):
client.get_market_liquidity([])
def test_get_market_liquidity_varied_periods(self):
"""Test market liquidity with varied time periods."""
from datetime import datetime, timedelta
client = GovmapClient()
# Deals spread across recent quarters - now using Deal models
@@ -514,7 +551,7 @@ class TestMarketAnalysisFunctions:
Deal(
objectid=i,
deal_date=(today - timedelta(days=days)).strftime("%Y-%m-%d"),
deal_amount=1000000
deal_amount=1000000,
)
for i, days in enumerate([30, 60, 150, 240, 330]) # Spread across ~11 months
]
@@ -532,9 +569,27 @@ class TestMarketAnalysisFunctions:
# Now using Deal models
deals = [
Deal(objectid=1, property_type_description="דירה", rooms=3, deal_amount=1000000, deal_date="2023-01-01"),
Deal(objectid=2, property_type_description="בית", rooms=5, deal_amount=2000000, deal_date="2023-01-01"),
Deal(objectid=3, property_type_description="דירה", rooms=4, deal_amount=1500000, deal_date="2023-01-01"),
Deal(
objectid=1,
property_type_description="דירה",
rooms=3,
deal_amount=1000000,
deal_date="2023-01-01",
),
Deal(
objectid=2,
property_type_description="בית",
rooms=5,
deal_amount=2000000,
deal_date="2023-01-01",
),
Deal(
objectid=3,
property_type_description="דירה",
rooms=4,
deal_amount=1500000,
deal_date="2023-01-01",
),
]
filtered = client.filter_deals_by_criteria(deals, property_type="דירה")
@@ -551,7 +606,9 @@ class TestMarketAnalysisFunctions:
# Now using Deal models
deals = [
Deal(objectid=i, rooms=rooms, deal_amount=amount, deal_date="2023-01-01")
for i, (rooms, amount) in enumerate([(2, 800000), (3, 1000000), (4, 1500000), (5, 2000000)])
for i, (rooms, amount) in enumerate(
[(2, 800000), (3, 1000000), (4, 1500000), (5, 2000000)]
)
]
filtered = client.filter_deals_by_criteria(deals, min_rooms=3, max_rooms=4)
@@ -579,6 +636,7 @@ class TestMarketAnalysisFunctions:
def test_calculate_deal_statistics_success(self):
"""Test successful deal statistics calculation."""
from nadlan_mcp.govmap.models import DealStatistics
client = GovmapClient()
# Now using Deal models - price_per_sqm computed automatically
@@ -620,10 +678,27 @@ class TestMarketAnalysisFunctions:
"""Test that deals with missing property type are excluded when filter is active."""
client = GovmapClient()
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2023-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1000000, deal_date="2023-01-01", property_type_description=None),
Deal(objectid=3, deal_amount=1000000, deal_date="2023-01-01", property_type_description="בית"),
Deal(objectid=4, deal_amount=1000000, deal_date="2023-01-01"), # Missing property_type_description
Deal(
objectid=1,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description=None,
),
Deal(
objectid=3,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description="בית",
),
Deal(
objectid=4, deal_amount=1000000, deal_date="2023-01-01"
), # Missing property_type_description
]
filtered = client.filter_deals_by_criteria(deals, property_type="דירה")
@@ -684,10 +759,18 @@ class TestMarketAnalysisFunctions:
# This test now verifies filtering based on numeric ranges
client = GovmapClient()
deals = [
Deal(objectid=1, deal_amount=2000000, deal_date="2023-01-01", asset_area=65.0, rooms=3.0),
Deal(objectid=2, deal_amount=2000000, deal_date="2023-01-01", asset_area=80.0, rooms=3.0), # Area too high
Deal(objectid=3, deal_amount=2000000, deal_date="2023-01-01", asset_area=65.0, rooms=5.0), # Rooms too high
Deal(objectid=4, deal_amount=3000000, deal_date="2023-01-01", asset_area=65.0, rooms=3.0), # Price too high
Deal(
objectid=1, deal_amount=2000000, deal_date="2023-01-01", asset_area=65.0, rooms=3.0
),
Deal(
objectid=2, deal_amount=2000000, deal_date="2023-01-01", asset_area=80.0, rooms=3.0
), # Area too high
Deal(
objectid=3, deal_amount=2000000, deal_date="2023-01-01", asset_area=65.0, rooms=5.0
), # Rooms too high
Deal(
objectid=4, deal_amount=3000000, deal_date="2023-01-01", asset_area=65.0, rooms=3.0
), # Price too high
]
# Area filter should exclude deal 2
@@ -701,7 +784,9 @@ class TestMarketAnalysisFunctions:
assert all(d.objectid in [1, 2, 4] for d in filtered_rooms)
# Price filter should exclude deal 4
filtered_price = client.filter_deals_by_criteria(deals, min_price=1500000, max_price=2500000)
filtered_price = client.filter_deals_by_criteria(
deals, min_price=1500000, max_price=2500000
)
assert len(filtered_price) == 3
assert all(d.objectid in [1, 2, 3] for d in filtered_price)
@@ -709,9 +794,26 @@ class TestMarketAnalysisFunctions:
"""Test that deals with missing data pass through when no filter is active for that field."""
client = GovmapClient()
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2023-01-01", property_type_description="דירה", asset_area=65.0),
Deal(objectid=2, deal_amount=1000000, deal_date="2023-01-01", property_type_description="דירה", asset_area=None),
Deal(objectid=3, deal_amount=1000000, deal_date="2023-01-01", property_type_description="דירה"), # Missing asset_area
Deal(
objectid=1,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description="דירה",
asset_area=65.0,
),
Deal(
objectid=2,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description="דירה",
asset_area=None,
),
Deal(
objectid=3,
deal_amount=1000000,
deal_date="2023-01-01",
property_type_description="דירה",
), # Missing asset_area
]
# Filter by property type only - missing area should pass through
+23 -17
View File
@@ -6,19 +6,20 @@ without hitting the real Govmap API.
For full E2E tests with real API calls, see tests/e2e/test_mcp_tools.py
"""
import json
import pytest
from pathlib import Path
from unittest.mock import patch, Mock
from nadlan_mcp.govmap.models import Deal, AutocompleteResult, AutocompleteResponse, CoordinatePoint
from unittest.mock import patch
import pytest
from nadlan_mcp.fastmcp_server import (
autocomplete_address,
find_recent_deals_for_address,
get_street_deals,
get_neighborhood_deals,
get_deals_by_radius,
get_neighborhood_deals,
get_street_deals,
)
from nadlan_mcp.govmap.models import AutocompleteResponse, AutocompleteResult, Deal
# Load fixtures
FIXTURES_DIR = Path(__file__).parent / "fixtures"
@@ -61,7 +62,7 @@ def mock_polygon_metadata_data():
class TestMCPToolsFast:
"""Fast unit tests for MCP tools using mocked data."""
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_autocomplete_address_fast(self, mock_client, mock_autocomplete_data):
"""Test autocomplete with cached data."""
mock_client.autocomplete_address.return_value = mock_autocomplete_data
@@ -74,7 +75,7 @@ class TestMCPToolsFast:
assert "text" in data[0]
assert "coordinates" in data[0]
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_street_deals_fast(self, mock_client, mock_street_deals_data):
"""Test street deals with cached data."""
mock_client.get_street_deals.return_value = mock_street_deals_data
@@ -92,7 +93,7 @@ class TestMCPToolsFast:
assert "deal_amount" in deal
assert "deal_date" in deal
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_neighborhood_deals_fast(self, mock_client, mock_neighborhood_deals_data):
"""Test neighborhood deals with cached data."""
mock_client.get_neighborhood_deals.return_value = mock_neighborhood_deals_data
@@ -104,7 +105,7 @@ class TestMCPToolsFast:
assert data["total_deals"] == len(mock_neighborhood_deals_data)
assert "deals" in data
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_get_deals_by_radius_fast(self, mock_client, mock_polygon_metadata_data):
"""Test deals by radius with cached data."""
mock_client.get_deals_by_radius.return_value = mock_polygon_metadata_data
@@ -117,10 +118,15 @@ class TestMCPToolsFast:
assert "polygons" in data
@pytest.mark.skip(reason="Complex workflow with statistics - tested in E2E suite")
@patch('nadlan_mcp.fastmcp_server.client')
def test_find_recent_deals_fast(self, mock_client, mock_street_deals_data,
mock_neighborhood_deals_data, mock_polygon_metadata_data,
mock_autocomplete_data):
@patch("nadlan_mcp.fastmcp_server.client")
def test_find_recent_deals_fast(
self,
mock_client,
mock_street_deals_data,
mock_neighborhood_deals_data,
mock_polygon_metadata_data,
mock_autocomplete_data,
):
"""Test find_recent_deals with fully mocked workflow.
NOTE: This test is skipped because find_recent_deals_for_address has complex
@@ -129,7 +135,7 @@ class TestMCPToolsFast:
"""
pass
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_no_deals_found(self, mock_client):
"""Test handling when no deals are found."""
mock_client.get_street_deals.return_value = []
@@ -140,7 +146,7 @@ class TestMCPToolsFast:
assert isinstance(result, str)
assert "No" in result or "found" in result.lower()
@patch('nadlan_mcp.fastmcp_server.client')
@patch("nadlan_mcp.fastmcp_server.client")
def test_deal_type_filtering(self, mock_client, mock_street_deals_data):
"""Test that deal_type parameter is passed correctly."""
mock_client.get_street_deals.return_value = mock_street_deals_data
+6 -12
View File
@@ -10,26 +10,20 @@ import vcr
# Configure VCR instance
my_vcr = vcr.VCR(
# Store cassettes in tests/cassettes/ directory
cassette_library_dir='tests/cassettes',
cassette_library_dir="tests/cassettes",
# Record mode: once = record once, then replay
# Use 'new_episodes' to record new interactions but replay existing ones
record_mode='once',
record_mode="once",
# Match requests by method and URI
match_on=['method', 'scheme', 'host', 'port', 'path', 'query'],
match_on=["method", "scheme", "host", "port", "path", "query"],
# Filter out sensitive data from recordings
filter_headers=['authorization', 'x-api-key'],
filter_headers=["authorization", "x-api-key"],
# Decode compressed responses for better diffs
decode_compressed_response=True,
# Serialize as YAML for human-readable diffs
serializer='yaml',
serializer="yaml",
# Path transformer to organize cassettes
path_transformer=vcr.VCR.ensure_suffix('.yaml'),
path_transformer=vcr.VCR.ensure_suffix(".yaml"),
)