Testing update

This commit is contained in:
Nitzan Pomerantz
2025-10-27 23:33:09 +02:00
parent 247ddad8cd
commit a234e809bf
9 changed files with 1346 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
# Testing Strategy
## Overview
Nadlan-MCP uses a three-tier testing approach:
1. **Fast unit tests** - Cached fixtures, run in <1s (default)
2. **E2E smoke tests** - Minimal API calls, run in ~5s (verify API works)
3. **Comprehensive E2E tests** - Full API coverage, run in ~5min (optional)
## Running Tests
### Default: Fast Unit Tests Only
```bash
pytest tests/ --ignore=tests/e2e/
# Result: 180 passed, 1 skipped in 0.39s
```
### E2E Smoke Tests (Fast)
```bash
pytest tests/e2e/test_mcp_tools.py -m integration
# Result: 4 passed in 4.77s
```
### Comprehensive E2E Tests (Slow - Optional)
```bash
pytest tests/e2e/test_mcp_tools_comprehensive.py -m integration
# Result: 11 passed in 5m36s
```
### All Tests
```bash
pytest tests/
```
## Test Structure
### Fast Unit Tests (`tests/`)
- **174 tests** covering all core functionality
- Use cached API responses from `tests/fixtures/`
- Mock `GovmapClient` to avoid real API calls
- Run in **0.39 seconds**
Key test files:
- `tests/govmap/test_models.py` - Pydantic model validation (36 tests)
- `tests/govmap/test_utils.py` - Helper functions (45 tests)
- `tests/govmap/test_validators.py` - Input validation (24 tests)
- `tests/test_govmap_client.py` - Client and business logic (41 tests)
- `tests/test_fastmcp_tools.py` - MCP tool integration (22 tests)
- `tests/test_mcp_tools_fast.py` - Fast MCP tool tests with fixtures (6 tests)
### E2E Smoke Tests (`tests/e2e/test_mcp_tools.py`)
- **4 minimal smoke tests** with real API calls
- Verify API connectivity and basic functionality
- Use minimal data (small limits, small radius)
- Run in **4.77 seconds**
### Comprehensive E2E Tests (`tests/e2e/test_mcp_tools_comprehensive.py`)
- **11 thorough integration tests** with real API calls
- Test all 10 MCP tools with full data
- Verify complete workflow from MCP tool → API → response
- Catch API contract changes
- Run in **5min 36sec** (optional, use for releases)
## Fixtures
Cached API responses in `tests/fixtures/`:
- `autocomplete_response.json` - Address search results
- `street_deals.json` - Street-level deal data
- `neighborhood_deals.json` - Neighborhood-level deal data
- `polygon_metadata.json` - Polygon metadata from radius search
These fixtures are generated from real API calls and updated as needed.
## Test Markers
Configured in `pytest.ini`:
- `@pytest.mark.integration` - Slow tests requiring real API calls
- `@pytest.mark.unit` - Fast unit tests (default)
## Performance Comparison
| Test Suite | Count | Duration | Speed |
|------------|-------|----------|-------|
| Fast unit tests | 180 | 0.39s | 462 tests/sec |
| E2E smoke tests | 4 | 4.77s | 0.84 tests/sec |
| Comprehensive E2E | 11 | 5m36s | 0.033 tests/sec |
**Unit tests are 12x faster than smoke tests, 860x faster than comprehensive E2E!**
## CI/CD Recommendations
### PR Checks (~5s)
```bash
# Run unit tests + smoke tests
pytest tests/ --ignore=tests/e2e/test_mcp_tools_comprehensive.py
```
### Nightly Build (~6min)
```bash
# Run all tests including comprehensive E2E
pytest tests/
```
### Quick Verification (<5s)
```bash
# Just smoke tests to verify API works
pytest tests/e2e/test_mcp_tools.py -m integration
```
## Updating Fixtures
When API responses change, regenerate fixtures:
```bash
source venv/bin/activate
python << 'EOF'
import json
from nadlan_mcp.govmap import GovmapClient
client = GovmapClient()
# Update autocomplete fixture
autocomplete_response = client.autocomplete_address("חולון סוקולוב")
with open("tests/fixtures/autocomplete_response.json", "w", encoding="utf-8") as f:
json.dump([r.model_dump() for r in autocomplete_response.results], f, ensure_ascii=False, indent=2)
# Update street deals
street_deals = client.get_street_deals("52385050", limit=10)
with open("tests/fixtures/street_deals.json", "w", encoding="utf-8") as f:
json.dump([d.model_dump(mode='json') for d in street_deals], f, ensure_ascii=False, indent=2)
# Update neighborhood deals
neighborhood_deals = client.get_neighborhood_deals("52385050", limit=10)
with open("tests/fixtures/neighborhood_deals.json", "w", encoding="utf-8") as f:
json.dump([d.model_dump(mode='json') for d in neighborhood_deals], f, ensure_ascii=False, indent=2)
# Update polygon metadata
polygons = client.get_deals_by_radius((3870928.84, 3766290.19), radius=500)
with open("tests/fixtures/polygon_metadata.json", "w", encoding="utf-8") as f:
json.dump(polygons[:10], f, ensure_ascii=False, indent=2)
print("✓ Fixtures updated")
EOF
```
## Test Coverage
Run with coverage reporting:
```bash
pytest --cov=nadlan_mcp --cov-report=html
open htmlcov/index.html
```
+1
View File
@@ -0,0 +1 @@
"""End-to-end tests for MCP tools."""
+72
View File
@@ -0,0 +1,72 @@
"""
Fast E2E smoke tests for MCP tools (<30 seconds).
These tests make MINIMAL real API calls to verify the service is working.
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,
)
@pytest.mark.integration
class TestMCPToolsSmokeTests:
"""Minimal E2E smoke tests to verify API connectivity."""
# Known working data
TEST_ADDRESS = "סוקולוב 38 חולון"
TEST_POLYGON_ID = "52385050"
TEST_LAT = 3766290.19
TEST_LON = 3870928.84
def test_autocomplete_works(self):
"""Smoke test: Autocomplete returns results."""
result = autocomplete_address("חולון")
data = json.loads(result)
assert isinstance(data, list)
assert len(data) > 0
assert "text" in data[0]
def test_get_street_deals_works(self):
"""Smoke test: Can fetch street deals."""
# Use small limit for speed
result = get_street_deals(self.TEST_POLYGON_ID, limit=5, deal_type=2)
data = json.loads(result)
assert "total_deals" in data
assert data["total_deals"] > 0
assert "deals" in data
def test_get_deals_by_radius_works(self):
"""Smoke test: Can fetch polygon metadata by radius."""
# Use small radius for speed
result = get_deals_by_radius(self.TEST_LAT, self.TEST_LON, radius_meters=100)
data = json.loads(result)
assert "total_polygons" in data
assert data["total_polygons"] > 0
def test_find_recent_deals_minimal(self):
"""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
)
data = json.loads(result)
# Just verify structure, don't check counts
assert "search_parameters" in data
assert "market_statistics" in data
assert "deals" in data
assert isinstance(data["deals"], list)
+185
View File
@@ -0,0 +1,185 @@
"""
End-to-end tests for all MCP tools.
These tests make real API calls to verify the complete functionality
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,
compare_addresses,
get_street_deals,
get_neighborhood_deals,
get_deals_by_radius,
)
@pytest.mark.integration
class TestMCPToolsE2E:
"""End-to-end tests for all 10 MCP tools."""
# Test addresses
TEST_ADDRESS_1 = "סוקולוב 38 חולון"
TEST_ADDRESS_2 = "דיזנגוף 50 תל אביב"
TEST_POLYGON_ID = "52385050" # Known polygon with data
# ITM coordinates for Holon (lat, lon format for MCP tool)
TEST_LAT = 3766290.19
TEST_LON = 3870928.84
def test_autocomplete_address(self):
"""Test address autocomplete returns results."""
result = autocomplete_address("חולון סוקולוב")
data = json.loads(result)
assert isinstance(data, list)
assert len(data) > 0
# Check first result has expected structure
first = data[0]
assert "text" in first
assert "coordinates" in first or "id" in first
def test_find_recent_deals_for_address(self):
"""Test finding recent deals for an address."""
result = find_recent_deals_for_address(self.TEST_ADDRESS_1, max_deals=100)
data = json.loads(result)
# Check response structure
assert "search_parameters" in data
assert "market_statistics" in data
assert "deals" in data
# Verify some deals were found
assert isinstance(data["deals"], list)
assert len(data["deals"]) > 0
# Check deal structure
deal = data["deals"][0]
assert "deal_amount" in deal
assert "deal_date" in deal
assert "settlement_name_heb" in deal
def test_analyze_market_trends(self):
"""Test market trend analysis."""
result = analyze_market_trends(
self.TEST_ADDRESS_1, years_back=3, radius_meters=100
)
data = json.loads(result)
# Check response structure
assert "market_summary" in data
assert "total_deals" in data["market_summary"]
assert isinstance(data["market_summary"]["total_deals"], int)
assert data["market_summary"]["total_deals"] >= 0
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
)
data = json.loads(result)
assert "total_comparables" in data
assert isinstance(data["total_comparables"], int)
assert data["total_comparables"] >= 0
if data["total_comparables"] > 0:
assert "comparables" in data
comp = data["comparables"][0]
assert "deal_amount" in comp
assert "asset_room_num" in comp
def test_get_deal_statistics(self):
"""Test deal statistics calculation."""
result = get_deal_statistics(self.TEST_ADDRESS_1, years_back=3)
data = json.loads(result)
# Check response structure
assert "statistics" in data
if "sample_size" in data["statistics"]:
assert isinstance(data["statistics"]["sample_size"], int)
assert data["statistics"]["sample_size"] >= 0
def test_get_market_activity_metrics(self):
"""Test market activity metrics."""
result = get_market_activity_metrics(self.TEST_ADDRESS_1, years_back=3)
data = json.loads(result)
# Check response structure
assert "market_activity" in data
if data["market_activity"] is not None:
assert "activity_score" in data["market_activity"]
def test_compare_addresses(self):
"""Test comparing multiple addresses."""
result = compare_addresses([self.TEST_ADDRESS_1, self.TEST_ADDRESS_2])
data = json.loads(result)
assert isinstance(data, dict)
assert "addresses_compared" in data
assert "all_results" in data
def test_get_street_deals(self):
"""Test getting street-level deals."""
result = get_street_deals(self.TEST_POLYGON_ID, limit=100, deal_type=2)
data = json.loads(result)
assert "total_deals" in data
assert isinstance(data["total_deals"], int)
assert data["total_deals"] > 0 # Known polygon should have deals
assert "deals" in data
assert len(data["deals"]) > 0
# Check deal structure
deal = data["deals"][0]
assert "deal_amount" in deal
assert "deal_date" in deal
def test_get_neighborhood_deals(self):
"""Test getting neighborhood-level deals."""
result = get_neighborhood_deals(self.TEST_POLYGON_ID, limit=100, deal_type=2)
data = json.loads(result)
assert "total_deals" in data
assert isinstance(data["total_deals"], int)
assert data["total_deals"] > 0 # Known polygon should have deals
assert "deals" in data
assert len(data["deals"]) > 0
def test_get_deals_by_radius(self):
"""Test getting polygon metadata by radius."""
result = get_deals_by_radius(self.TEST_LAT, self.TEST_LON, radius_meters=500)
data = json.loads(result)
assert "total_polygons" in data
assert isinstance(data["total_polygons"], int)
assert data["total_polygons"] > 0 # Known coords should have polygons
assert "polygons" in data
assert len(data["polygons"]) > 0
# Check polygon metadata structure
polygon = data["polygons"][0]
assert "polygon_id" in polygon or "objectid" in polygon
def test_get_deals_by_radius_no_results(self):
"""Test get_deals_by_radius with coords that have no data."""
# Use coordinates unlikely to have data
result = get_deals_by_radius(37.66, 38.71, radius_meters=10)
# Should return a message string, not JSON
assert isinstance(result, str)
assert "No polygons found" in result or "total_polygons" in result
+35
View File
@@ -0,0 +1,35 @@
[
{
"text": "סוקולוב חולון",
"id": "street|STREET_MID_POINT|40087",
"type": "street",
"score": 1658.0989,
"coordinates": {
"longitude": 3871467.9873431968,
"latitude": 3766276.9742487124
},
"shape": "POINT(3871467.9873431968 3766276.9742487124)"
},
{
"text": "קופת חולים - לאומית סוקולוב חולון",
"id": "institutes|POI_BLDG|71871",
"type": "institutes",
"score": 1095.5675,
"coordinates": {
"longitude": 3871143.6159368134,
"latitude": 3766329.3192062005
},
"shape": "POINT(3871143.6159368134 3766329.3192062005)"
},
{
"text": "קופת חולים - לאומית סוקולוב חולון",
"id": "poi|POI_MID_POINT|102233",
"type": "poi",
"score": 1099.5448,
"coordinates": {
"longitude": 3871143.6159681133,
"latitude": 3766329.319199102
},
"shape": "POINT(3871143.6159681133 3766329.319199102)"
}
]
+332
View File
@@ -0,0 +1,332 @@
[
{
"objectid": 1969420,
"deal_amount": 1600000.0,
"deal_date": "2025-09-11",
"asset_area": 61.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870720.4640780594 3766261.471274853,3870704.822454856 3766280.494942658,3870715.8743646624 3766289.5618421077,3870731.3745853226 3766270.5375762666,3870720.4640780594 3766261.471274853)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000721,
"streetNameHeb": "בילינסון",
"streetNameEng": "Belinson",
"houseNum": 6,
"floorNo": "רביעית",
"dealId": 3633677206,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7172,
"parcelNum": 235,
"subParcelNum": 11,
"polygonId": "52995209",
"price_per_sqm": 26229.51
},
{
"objectid": 1968034,
"deal_amount": 1800000.0,
"deal_date": "2025-08-21",
"asset_area": 51.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3871546.267254118 3766046.2332329005,3871519.7450213963 3766048.8632871644,3871520.755660357 3766058.564386736,3871529.006896269 3766057.780093892,3871529.5742626246 3766063.3353820755,3871547.727459817 3766061.4891484776,3871546.267254118 3766046.2332329005)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000415,
"streetNameHeb": "חנקין",
"streetNameEng": "Hankin",
"houseNum": 62,
"floorNo": "שניה",
"dealId": 3632991008,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7170,
"parcelNum": 91,
"subParcelNum": 8,
"polygonId": "52487416",
"price_per_sqm": 35294.12
},
{
"objectid": 1972678,
"deal_amount": 2170000.0,
"deal_date": "2025-08-20",
"asset_area": 90.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870461.085939886 3765412.661079392,3870461.8556948095 3765419.4469509623,3870454.5119660264 3765416.0582189877,3870449.8311605644 3765416.7315218183,3870448.8832249534 3765412.6281236825,3870444.1110105794 3765413.266600847,3870448.3403304657 3765432.7404882894,3870508.9368100218 3765424.106078474,3870506.5193002103 3765406.631910944,3870482.4682015995 3765409.8236224824,3870476.137660146 3765410.663707863,3870461.085939886 3765412.661079392)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000720,
"streetNameHeb": "לאון בלום",
"streetNameEng": "L?on Blum",
"houseNum": 64,
"floorNo": "שניה",
"dealId": 3633156134,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 4,
"gushNum": 7176,
"parcelNum": 9,
"subParcelNum": 59,
"polygonId": "52202717",
"price_per_sqm": 24111.11
},
{
"objectid": 1967412,
"deal_amount": 2050000.0,
"deal_date": "2025-08-14",
"asset_area": 95.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3871347.8555616946 3766162.0722470563,3871334.99564493 3766163.205144352,3871336.9037298146 3766187.3432294633,3871349.869159977 3766186.35283101,3871347.8555616946 3766162.0722470563)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000443,
"streetNameHeb": "שפרינצק",
"streetNameEng": "Sprinzak",
"houseNum": 29,
"floorNo": "שניה",
"dealId": 3632926686,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 4,
"gushNum": 7170,
"parcelNum": 28,
"subParcelNum": 6,
"polygonId": "53339618",
"price_per_sqm": 21578.95
},
{
"objectid": 1972438,
"deal_amount": 2057000.0,
"deal_date": "2025-08-13",
"asset_area": 70.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870628.707375865 3765928.3916899217,3870616.197248105 3765933.4442600156,3870624.479632951 3765954.33986368,3870628.9524073373 3765952.58190368,3870629.4304386983 3765953.8507194677,3870634.044605639 3765952.0933281765,3870632.98273369 3765949.4960702136,3870635.237077585 3765948.5461212373,3870632.1913898066 3765941.121957529,3870633.501643529 3765940.5352417007,3870628.707375865 3765928.3916899217)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000720,
"streetNameHeb": "לאון בלום",
"streetNameEng": "L?on Blum",
"houseNum": 23,
"floorNo": null,
"dealId": 3633759218,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7176,
"parcelNum": 23,
"subParcelNum": 2,
"polygonId": "52475840",
"price_per_sqm": 29385.71
},
{
"objectid": 1972437,
"deal_amount": 2207000.0,
"deal_date": "2025-08-13",
"asset_area": 78.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870628.707375865 3765928.3916899217,3870616.197248105 3765933.4442600156,3870624.479632951 3765954.33986368,3870628.9524073373 3765952.58190368,3870629.4304386983 3765953.8507194677,3870634.044605639 3765952.0933281765,3870632.98273369 3765949.4960702136,3870635.237077585 3765948.5461212373,3870632.1913898066 3765941.121957529,3870633.501643529 3765940.5352417007,3870628.707375865 3765928.3916899217)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000720,
"streetNameHeb": "לאון בלום",
"streetNameEng": "L?on Blum",
"houseNum": 23,
"floorNo": null,
"dealId": 3633751330,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 4,
"gushNum": 7176,
"parcelNum": 23,
"subParcelNum": 2,
"polygonId": "52475840",
"price_per_sqm": 28294.87
},
{
"objectid": 1967707,
"deal_amount": 1400000.0,
"deal_date": "2025-08-12",
"asset_area": 74.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3871406.5046315272 3766032.4932356784,3871393.876109203 3766033.069669732,3871395.3254547855 3766060.893880203,3871408.6494925357 3766060.5448129214,3871406.5046315272 3766032.4932356784)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000503,
"streetNameHeb": "ביאליק",
"streetNameEng": "Bialik",
"houseNum": 8,
"floorNo": "שניה",
"dealId": 3633004406,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7170,
"parcelNum": 57,
"subParcelNum": 6,
"polygonId": "64907781",
"price_per_sqm": 18918.92
},
{
"objectid": 1971495,
"deal_amount": 1000000.0,
"deal_date": "2025-08-07",
"asset_area": 82.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3871176.244711883 3765574.7175676874,3871108.214092787 3765583.100809277,3871109.35308285 3765593.027038448,3871177.5128026525 3765584.7863754295,3871176.244711883 3765574.7175676874)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "HOLON",
"streetCode": 66000304,
"streetNameHeb": "ההסתדרות",
"streetNameEng": "HaHistadrut",
"houseNum": 60,
"floorNo": "ראשונה",
"dealId": 3632654898,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 4,
"gushNum": 7174,
"parcelNum": 199,
"subParcelNum": 27,
"polygonId": "52506738",
"price_per_sqm": 12195.12
},
{
"objectid": 1969515,
"deal_amount": 3000000.0,
"deal_date": "2025-08-06",
"asset_area": 117.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870582.7778358534 3766007.818947901,3870555.75388674 3766020.9588606944,3870560.5454790723 3766030.81737579,3870587.5814648867 3766017.6182896625,3870582.7778358534 3766007.818947901)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000721,
"streetNameHeb": "בילינסון",
"streetNameEng": "Belinson",
"houseNum": 35,
"floorNo": "מרתף+",
"dealId": 3632769506,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 5,
"gushNum": 7172,
"parcelNum": 55,
"subParcelNum": 2,
"polygonId": "52469890",
"price_per_sqm": 25641.03
},
{
"objectid": 1967553,
"deal_amount": 3540000.0,
"deal_date": "2025-08-04",
"asset_area": 130.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3871283.98382443 3766041.272967665,3871272.3244377715 3766042.777610746,3871274.3714343957 3766058.5450168313,3871278.7452240535 3766057.970421447,3871282.8507001335 3766089.5882179793,3871278.6182943583 3766090.1633791463,3871280.9063065085 3766107.565754848,3871297.410878644 3766105.4883675845,3871296.839837455 3766100.8920923057,3871290.697705125 3766101.684622215,3871289.325197258 3766091.1650558305,3871293.4512298624 3766090.672348892,3871291.964111744 3766079.3353630104,3871299.8747903355 3766078.2657070043,3871298.260388772 3766066.3362180092,3871291.1870977194 3766067.2079217676,3871289.8131462242 3766057.05542465,3871286.0290623982 3766057.490289319,3871283.98382443 3766041.272967665)))",
"source_polygon_id": null,
"sourceorder": null,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000708,
"streetNameHeb": "שנקר",
"streetNameEng": "Shenkar",
"houseNum": 55,
"floorNo": "שניה",
"dealId": 3632695759,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 5,
"gushNum": 7170,
"parcelNum": 44,
"subParcelNum": 19,
"polygonId": "52507812",
"price_per_sqm": 27230.77
}
]
+82
View File
@@ -0,0 +1,82 @@
[
{
"dealscount": "10",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7170-31",
"objectid": 14248
},
{
"dealscount": "6",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-19",
"objectid": 14249
},
{
"dealscount": "1",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-3",
"objectid": 14301
},
{
"dealscount": "14",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-212",
"objectid": 14302
},
{
"dealscount": "14",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-213",
"objectid": 14303
},
{
"dealscount": "57",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-128",
"objectid": 14304
},
{
"dealscount": "1",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-5",
"objectid": 14305
},
{
"dealscount": "3",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-216",
"objectid": 14306
},
{
"dealscount": "11",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-218",
"objectid": 14314
},
{
"dealscount": "1",
"settlementNameHeb": "חולון",
"streetNameHeb": null,
"houseNum": null,
"polygon_id": "7172-7",
"objectid": 14315
}
]
+332
View File
@@ -0,0 +1,332 @@
[
{
"objectid": 1960644,
"deal_amount": 200000.0,
"deal_date": "2024-10-13",
"asset_area": 20.0,
"settlement_name_heb": "חולון",
"property_type_description": "בנין",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "קרקע",
"dealId": 3605441588,
"dealNatureDescription": "חנות",
"assetRoomNum": null,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 5,
"polygonId": "52385050",
"price_per_sqm": 10000.0
},
{
"objectid": 1960654,
"deal_amount": 892000.0,
"deal_date": "2024-08-08",
"asset_area": 69.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 34,
"floorNo": "שניה",
"dealId": 3603340277,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7166,
"parcelNum": 142,
"subParcelNum": 8,
"polygonId": "52385050",
"price_per_sqm": 12927.54
},
{
"objectid": 1960646,
"deal_amount": 1650000.0,
"deal_date": "2022-08-17",
"asset_area": 59.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "ראשונה",
"dealId": 3547500052,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 2,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 7,
"polygonId": "52385050",
"price_per_sqm": 27966.1
},
{
"objectid": 1960650,
"deal_amount": 500000.0,
"deal_date": "2019-03-03",
"asset_area": 29.0,
"settlement_name_heb": "חולון",
"property_type_description": "בנין",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 34,
"floorNo": "קרקע",
"dealId": 3453745430,
"dealNatureDescription": "חנות",
"assetRoomNum": null,
"gushNum": 7166,
"parcelNum": 142,
"subParcelNum": 4,
"polygonId": "52385050",
"price_per_sqm": 17241.38
},
{
"objectid": 1960640,
"deal_amount": 510000.0,
"deal_date": "2016-12-06",
"asset_area": 98.0,
"settlement_name_heb": "חולון",
"property_type_description": "בנין",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "מרתף",
"dealId": 3375045810,
"dealNatureDescription": "מחסנים",
"assetRoomNum": null,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 1,
"polygonId": "52385050",
"price_per_sqm": 5204.08
},
{
"objectid": 1960642,
"deal_amount": 899000.0,
"deal_date": "2013-12-30",
"asset_area": 42.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "שלישית",
"dealId": 3306035061,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 2,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 11,
"polygonId": "52385050",
"price_per_sqm": 21404.76
},
{
"objectid": 1960638,
"deal_amount": 580000.0,
"deal_date": "2013-03-11",
"asset_area": 47.0,
"settlement_name_heb": "חולון",
"property_type_description": "בנין",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "מרתף",
"dealId": 3280438878,
"dealNatureDescription": "תעשיה",
"assetRoomNum": null,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 1,
"polygonId": "52385050",
"price_per_sqm": 12340.43
},
{
"objectid": 1960649,
"deal_amount": 720000.0,
"deal_date": "2008-11-20",
"asset_area": 20.0,
"settlement_name_heb": "חולון",
"property_type_description": "בנין",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 34,
"floorNo": "קרקע",
"dealId": 3140497058,
"dealNatureDescription": "חנות",
"assetRoomNum": null,
"gushNum": 7166,
"parcelNum": 142,
"subParcelNum": 3,
"polygonId": "52385050",
"price_per_sqm": 36000.0
},
{
"objectid": 1960643,
"deal_amount": 100000.0,
"deal_date": "2008-11-09",
"asset_area": 58.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "שלישית",
"dealId": 3139951568,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 3,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 12,
"polygonId": "52385050",
"price_per_sqm": 1724.14
},
{
"objectid": 1960647,
"deal_amount": 430000.0,
"deal_date": "2008-06-17",
"asset_area": 42.0,
"settlement_name_heb": "חולון",
"property_type_description": "דירה",
"neighborhood": "קרית עבודה",
"street_name": null,
"house_number": null,
"floor": null,
"floor_number": null,
"rooms": null,
"priority": null,
"shape": "MULTIPOLYGON(((3870903.639498367 3766273.3642689525,3870887.6103521264 3766277.301987773,3870893.7433254845 3766302.5584789426,3870909.784521754 3766298.561590301,3870903.639498367 3766273.3642689525)))",
"source_polygon_id": null,
"sourceorder": 1,
"settlementId": 6600,
"settlementNameEng": "Holon",
"streetCode": 66000401,
"streetNameHeb": "סוקולוב",
"streetNameEng": "Sokolov",
"houseNum": 32,
"floorNo": "שניה",
"dealId": 3137311234,
"dealNatureDescription": "דירה בבית קומות",
"assetRoomNum": 2,
"gushNum": 7166,
"parcelNum": 141,
"subParcelNum": 9,
"polygonId": "52385050",
"price_per_sqm": 10238.1
}
]
+155
View File
@@ -0,0 +1,155 @@
"""
Fast unit tests for MCP tools using cached fixtures.
These tests use pre-recorded API responses to run quickly (~1s)
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 nadlan_mcp.fastmcp_server import (
autocomplete_address,
find_recent_deals_for_address,
get_street_deals,
get_neighborhood_deals,
get_deals_by_radius,
)
# Load fixtures
FIXTURES_DIR = Path(__file__).parent / "fixtures"
def load_fixture(filename):
"""Load a JSON fixture file."""
with open(FIXTURES_DIR / filename, encoding="utf-8") as f:
return json.load(f)
@pytest.fixture
def mock_autocomplete_data():
"""Cached autocomplete response."""
data = load_fixture("autocomplete_response.json")
results = [AutocompleteResult.model_validate(r) for r in data]
return AutocompleteResponse(results=results, resultsCount=len(results))
@pytest.fixture
def mock_street_deals_data():
"""Cached street deals response."""
data = load_fixture("street_deals.json")
return [Deal.model_validate(d) for d in data]
@pytest.fixture
def mock_neighborhood_deals_data():
"""Cached neighborhood deals response."""
data = load_fixture("neighborhood_deals.json")
return [Deal.model_validate(d) for d in data]
@pytest.fixture
def mock_polygon_metadata_data():
"""Cached polygon metadata response."""
return load_fixture("polygon_metadata.json")
class TestMCPToolsFast:
"""Fast unit tests for MCP tools using mocked data."""
@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
result = autocomplete_address("חולון סוקולוב")
data = json.loads(result)
assert isinstance(data, list)
assert len(data) > 0
assert "text" in data[0]
assert "coordinates" in data[0]
@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
result = get_street_deals("52385050", limit=100, deal_type=2)
data = json.loads(result)
assert "total_deals" in data
assert data["total_deals"] == len(mock_street_deals_data)
assert "deals" in data
assert len(data["deals"]) > 0
# Verify deal structure
deal = data["deals"][0]
assert "deal_amount" in deal
assert "deal_date" in deal
@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
result = get_neighborhood_deals("52385050", limit=100, deal_type=2)
data = json.loads(result)
assert "total_deals" in data
assert data["total_deals"] == len(mock_neighborhood_deals_data)
assert "deals" in data
@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
result = get_deals_by_radius(37.6629, 38.7093, radius_meters=500)
data = json.loads(result)
assert "total_polygons" in data
assert data["total_polygons"] == len(mock_polygon_metadata_data)
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):
"""Test find_recent_deals with fully mocked workflow.
NOTE: This test is skipped because find_recent_deals_for_address has complex
statistics calculations that require specific data structure. Use E2E tests
in tests/e2e/test_mcp_tools.py for full integration testing.
"""
pass
@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 = []
result = get_street_deals("999999", limit=100, deal_type=2)
# Should return a message string when no deals found
assert isinstance(result, str)
assert "No" in result or "found" in result.lower()
@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
# Test with deal_type=1 (first hand)
result = get_street_deals("52385050", limit=100, deal_type=1)
data = json.loads(result)
# Verify client was called with correct deal_type
mock_client.get_street_deals.assert_called_once_with("52385050", 100, deal_type=1)
assert "deal_type" in data
assert data["deal_type"] == 1