"""Tests for source modules — uses respx to mock HTTP calls."""
import json
from pathlib import Path
from unittest.mock import AsyncMock, patch
import httpx
import pytest
import respx
from world_intel_mcp.cache import Cache
from world_intel_mcp.circuit_breaker import CircuitBreaker
from world_intel_mcp.fetcher import Fetcher
@pytest.fixture
def cache(tmp_path: Path) -> Cache:
return Cache(db_path=tmp_path / "test_cache.db")
@pytest.fixture
def fetcher(cache: Cache) -> Fetcher:
breaker = CircuitBreaker()
return Fetcher(cache=cache, breaker=breaker, default_timeout=5.0)
# ---------------------------------------------------------------------------
# Markets
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_market_quotes(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_market_quotes
# Mock Yahoo Finance v8 chart response for ^GSPC
chart_response = {
"chart": {
"result": [{
"meta": {
"symbol": "^GSPC",
"regularMarketPrice": 5123.45,
"regularMarketChangePercent": 0.42,
"currency": "USD",
}
}]
}
}
respx.get("https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC").mock(
return_value=httpx.Response(200, json=chart_response)
)
result = await fetch_market_quotes(fetcher, symbols=["^GSPC"])
assert "quotes" in result
assert len(result["quotes"]) == 1
assert result["quotes"][0]["symbol"] == "^GSPC"
assert result["quotes"][0]["price"] == 5123.45
assert result["source"] == "yahoo-finance"
@respx.mock
@pytest.mark.asyncio
async def test_fetch_crypto_quotes(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_crypto_quotes
coins = [
{
"id": "bitcoin",
"symbol": "btc",
"name": "Bitcoin",
"current_price": 98000,
"market_cap": 1900000000000,
"price_change_percentage_24h": 2.5,
"sparkline_in_7d": {"price": [95000, 96000, 97000, 98000]},
}
]
respx.get("https://api.coingecko.com/api/v3/coins/markets").mock(
return_value=httpx.Response(200, json=coins)
)
result = await fetch_crypto_quotes(fetcher, limit=5)
assert "coins" in result
assert len(result["coins"]) == 1
assert result["coins"][0]["symbol"] == "btc"
assert result["source"] == "coingecko"
# ---------------------------------------------------------------------------
# Seismology
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_earthquakes(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.seismology import fetch_earthquakes
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "us7000abc1",
"properties": {
"mag": 5.2,
"place": "100km SSW of Somewhere",
"time": 1708700000000,
"tsunami": 0,
"felt": 15,
"alert": "green",
"url": "https://earthquake.usgs.gov/earthquakes/eventpage/us7000abc1",
},
"geometry": {
"type": "Point",
"coordinates": [-120.5, 35.2, 10.0],
},
}
],
}
respx.get("https://earthquake.usgs.gov/fdsnws/event/1/query").mock(
return_value=httpx.Response(200, json=geojson)
)
result = await fetch_earthquakes(fetcher, min_magnitude=4.0, hours=24)
assert result["count"] == 1
eq = result["earthquakes"][0]
assert eq["magnitude"] == 5.2
assert eq["id"] == "us7000abc1"
assert eq["depth_km"] == 10.0
assert eq["latitude"] == 35.2
assert eq["longitude"] == -120.5
assert result["source"] == "usgs"
# ---------------------------------------------------------------------------
# Wildfire
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_wildfires_no_api_key(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.wildfire import fetch_wildfires
with patch.dict("os.environ", {}, clear=False):
# Remove the key if present
import os
os.environ.pop("NASA_FIRMS_API_KEY", None)
result = await fetch_wildfires(fetcher, api_key=None)
assert "error" in result
@respx.mock
@pytest.mark.asyncio
async def test_fetch_wildfires_single_region(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.wildfire import fetch_wildfires
csv_data = (
"latitude,longitude,bright_ti4,scan,track,acq_date,acq_time,satellite,confidence,version,bright_ti5,frp,daynight\n"
"34.5,-118.2,350.0,0.5,0.5,2024-02-23,1200,N,high,2.0,290.0,45.0,D\n"
"34.6,-118.3,360.0,0.5,0.5,2024-02-23,1200,N,high,2.0,295.0,55.0,D\n"
"34.5,-118.2,340.0,0.5,0.5,2024-02-23,1200,N,low,2.0,285.0,30.0,D\n"
)
respx.get(url__regex=r".*firms\.modaps\.eosdis\.nasa\.gov.*").mock(
return_value=httpx.Response(200, text=csv_data)
)
result = await fetch_wildfires(fetcher, region="north_america", api_key="testkey")
assert "fires_by_region" in result
na = result["fires_by_region"]["north_america"]
assert na["count"] == 2 # only 2 high-confidence
assert result["total_fires"] == 2
# ---------------------------------------------------------------------------
# Economic
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_fred_series_no_key(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.economic import fetch_fred_series
import os
os.environ.pop("FRED_API_KEY", None)
result = await fetch_fred_series(fetcher, series_id="UNRATE", api_key=None)
assert "error" in result
@respx.mock
@pytest.mark.asyncio
async def test_fetch_world_bank_indicators(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.economic import fetch_world_bank_indicators
wb_response = [
{"page": 1, "pages": 1, "per_page": 5, "total": 2},
[
{"indicator": {"id": "NY.GDP.MKTP.CD", "value": "GDP"}, "date": "2023", "value": 25000000000000},
{"indicator": {"id": "NY.GDP.MKTP.CD", "value": "GDP"}, "date": "2022", "value": 24000000000000},
],
]
respx.get(url__regex=r".*api\.worldbank\.org.*").mock(
return_value=httpx.Response(200, json=wb_response)
)
result = await fetch_world_bank_indicators(fetcher, country="USA", indicators=["NY.GDP.MKTP.CD"])
assert "indicators" in result
assert len(result["indicators"]) == 1
assert result["indicators"][0]["id"] == "NY.GDP.MKTP.CD"
assert result["source"] == "world-bank"
# ---------------------------------------------------------------------------
# Health (disease outbreaks)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_disease_outbreaks(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.health import fetch_disease_outbreaks
rss_xml = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>WHO</title>
<item>
<title>Ebola outbreak in DRC - Update 5</title>
<link>https://who.int/ebola-update</link>
<pubDate>Mon, 10 Feb 2026 12:00:00 GMT</pubDate>
<description>Ebola virus disease outbreak continues in North Kivu.</description>
</item>
<item>
<title>Seasonal influenza update</title>
<link>https://who.int/flu</link>
<pubDate>Sun, 09 Feb 2026 08:00:00 GMT</pubDate>
<description>Northern hemisphere flu season report.</description>
</item>
</channel></rss>"""
# Mock all 3 health feeds returning same XML
respx.get(url__regex=r".*who\.int.*").mock(
return_value=httpx.Response(200, text=rss_xml)
)
respx.get(url__regex=r".*cdc\.gov.*").mock(
return_value=httpx.Response(200, text=rss_xml)
)
respx.get(url__regex=r".*outbreaknewstoday.*").mock(
return_value=httpx.Response(200, text=rss_xml)
)
result = await fetch_disease_outbreaks(fetcher)
assert result["source"] == "health-outbreak-monitor"
assert result["count"] > 0
assert result["high_concern_count"] > 0 # "ebola" in title
assert any(item["is_high_concern"] for item in result["items"])
# ---------------------------------------------------------------------------
# Sanctions (OFAC SDN)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_sanctions_search(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.sanctions import fetch_sanctions_search
csv_data = (
'100,"DOE, John",individual,SDGT,"","","","","","","","nationality Iran; DOB 01 Jan 1970"\n'
'101,"ACME CORP",entity,CUBA,"","","","","","","",""\n'
'102,"SMITH, Jane",individual,SYRIA,"","","","","","","","nationality Syria"\n'
)
respx.get(url__regex=r".*treasury\.gov.*sdn\.csv.*").mock(
return_value=httpx.Response(200, text=csv_data)
)
result = await fetch_sanctions_search(fetcher, query="DOE")
assert result["source"] == "ofac-sdn"
assert result["count"] == 1
assert result["matches"][0]["name"] == "DOE, John"
assert result["total_entities"] >= 1
@respx.mock
@pytest.mark.asyncio
async def test_fetch_sanctions_search_country_filter(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.sanctions import fetch_sanctions_search
csv_data = (
'100,"DOE, John",individual,SDGT,"","","","","","","","nationality Iran; DOB 01 Jan 1970"\n'
'101,"ACME CORP",entity,CUBA,"","","","","","","",""\n'
)
respx.get(url__regex=r".*treasury\.gov.*sdn\.csv.*").mock(
return_value=httpx.Response(200, text=csv_data)
)
result = await fetch_sanctions_search(fetcher, country="iran")
assert result["count"] == 1
assert result["matches"][0]["name"] == "DOE, John"
# ---------------------------------------------------------------------------
# Elections (pure data — no HTTP mocking needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_election_calendar(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.elections import fetch_election_calendar
result = await fetch_election_calendar(fetcher)
assert result["source"] == "election-calendar"
assert result["count"] > 0
assert "elections" in result
# Each election should have risk_score
for election in result["elections"]:
assert "risk_score" in election
assert "days_until" in election
assert election["status"] in ("past", "upcoming")
@pytest.mark.asyncio
async def test_fetch_election_calendar_country_filter(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.elections import fetch_election_calendar
result = await fetch_election_calendar(fetcher, country="USA")
# May or may not match depending on config data
assert result["source"] == "election-calendar"
assert isinstance(result["elections"], list)
# ---------------------------------------------------------------------------
# Shipping (Yahoo Finance quotes)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_shipping_index(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.shipping import fetch_shipping_index
chart_response = {
"chart": {
"result": [{
"meta": {
"symbol": "BDRY",
"regularMarketPrice": 15.50,
"regularMarketChangePercent": 4.2,
"currency": "USD",
}
}]
}
}
# Mock all 4 shipping symbols
respx.get(url__regex=r".*finance\.yahoo\.com.*").mock(
return_value=httpx.Response(200, json=chart_response)
)
result = await fetch_shipping_index(fetcher)
assert result["source"] == "yahoo-finance"
assert len(result["quotes"]) > 0
assert isinstance(result["stress_score"], (int, float))
assert result["assessment"] in ("low", "moderate", "elevated", "high", "extreme")
# ---------------------------------------------------------------------------
# Social (Reddit)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_social_signals(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.social import fetch_social_signals
reddit_response = {
"data": {
"children": [
{
"data": {
"title": "Ukraine conflict escalation analysis",
"score": 5000,
"num_comments": 300,
"upvote_ratio": 0.95,
"created_utc": 1708700000,
"permalink": "/r/worldnews/comments/abc123/",
"is_self": False,
}
},
{
"data": {
"title": "US-China trade tensions rise",
"score": 2000,
"num_comments": 150,
"upvote_ratio": 0.88,
"created_utc": 1708690000,
"permalink": "/r/worldnews/comments/def456/",
"is_self": True,
}
},
]
}
}
respx.get(url__regex=r".*reddit\.com.*hot\.json.*").mock(
return_value=httpx.Response(200, json=reddit_response)
)
result = await fetch_social_signals(fetcher)
assert result["source"] == "reddit-public"
assert result["velocity_metrics"]["total_posts"] > 0
assert result["velocity_metrics"]["high_engagement_count"] > 0
assert result["subreddits_queried"] == ["worldnews", "geopolitics"]
# ---------------------------------------------------------------------------
# Nuclear (USGS near test sites)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_nuclear_monitor(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.nuclear import fetch_nuclear_monitor
geojson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "nn00900001",
"properties": {
"mag": 2.8,
"place": "50km N of Test Site",
"time": 1708700000000,
"tsunami": 0,
},
"geometry": {
"type": "Point",
"coordinates": [129.08, 41.30, 3.0], # Near Punggye-ri
},
}
],
}
# Mock USGS for all nuclear sites
respx.get("https://earthquake.usgs.gov/fdsnws/event/1/query").mock(
return_value=httpx.Response(200, json=geojson)
)
result = await fetch_nuclear_monitor(fetcher, hours=72)
assert result["source"] == "usgs-nuclear-monitor"
assert len(result["sites"]) == 5 # 5 nuclear test sites
assert isinstance(result["total_flagged_events"], int)
assert isinstance(result["critical_flags"], int)
# ---------------------------------------------------------------------------
# Infrastructure (Cloudflare + IODA fallback)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_internet_outages_ioda_fallback(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.infrastructure import fetch_internet_outages
# IODA response shape
ioda_response = {
"data": [
{
"entity": {"code": "US", "name": "United States"},
"events": [
{
"id": "out-123",
"from": "2026-02-20T00:00:00Z",
"until": None,
"summary": "BGP outage detected",
"level": "country",
}
],
}
]
}
# Cloudflare returns 403 (no token)
respx.get(url__regex=r".*cloudflare\.com.*").mock(
return_value=httpx.Response(403, json={"error": "unauthorized"})
)
# IODA responds
respx.get(url__regex=r".*ioda\.inetintel.*").mock(
return_value=httpx.Response(200, json=ioda_response)
)
import os
os.environ.pop("CLOUDFLARE_API_TOKEN", None)
result = await fetch_internet_outages(fetcher)
assert result["source"] == "ioda-gatech"
assert result["total_7d"] == 1
assert result["ongoing_count"] == 1
@respx.mock
@pytest.mark.asyncio
async def test_fetch_cable_health(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.infrastructure import fetch_cable_health
warnings = [
{
"msgYear": 2026,
"msgNumber": 42,
"navArea": "XII",
"subregion": "31",
"status": "in force",
"issueDate": "2026-02-20",
"text": "SUBMARINE CABLE OPERATIONS 40-30.5N/030-15.2E VESSELS ADVISED",
}
]
respx.get(url__regex=r".*nga\.mil.*broadcast-warn.*").mock(
return_value=httpx.Response(200, json=warnings)
)
result = await fetch_cable_health(fetcher)
assert result["source"] == "nga-msi"
assert "corridors" in result
assert len(result["corridors"]) == 6
assert result["cable_related_warnings"] >= 1 # "cable" keyword in text
# ---------------------------------------------------------------------------
# Conflict (UCDP + ACLED)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_ucdp_events(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.conflict import fetch_ucdp_events
from datetime import datetime, timezone
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
ucdp_response = {
"TotalPages": 1,
"Result": [
{
"id": 12345,
"relid": "11-1",
"year": 2026,
"date_start": today,
"date_end": today,
"country": "Ukraine",
"region": "Europe",
"type_of_violence": 1,
"side_a": "Government of Ukraine",
"side_b": "DPR",
"best": 5,
"high": 10,
"low": 2,
"latitude": 48.0,
"longitude": 37.5,
"source_article": "Reuters",
"source_headline": "Fighting continues",
}
],
}
respx.get(url__regex=r".*ucdpapi\.pcr\.uu\.se.*").mock(
return_value=httpx.Response(200, json=ucdp_response)
)
result = await fetch_ucdp_events(fetcher, days=30)
assert result["source"] == "ucdp"
assert result["count"] == 1
assert result["events"][0]["country"] == "Ukraine"
assert result["total_fatalities_best"] == 5
# ---------------------------------------------------------------------------
# Hacker News
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_hacker_news(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.hacker_news import fetch_hacker_news
respx.get("https://hacker-news.firebaseio.com/v0/topstories.json").mock(
return_value=httpx.Response(200, json=[101, 102])
)
respx.get("https://hacker-news.firebaseio.com/v0/item/101.json").mock(
return_value=httpx.Response(200, json={
"id": 101, "title": "Show HN: AI Tool", "url": "https://example.com",
"score": 200, "by": "user1", "time": 1700000000, "descendants": 50,
})
)
respx.get("https://hacker-news.firebaseio.com/v0/item/102.json").mock(
return_value=httpx.Response(200, json={
"id": 102, "title": "Rust 2.0", "url": "https://example.com/rust",
"score": 150, "by": "user2", "time": 1700001000, "descendants": 30,
})
)
result = await fetch_hacker_news(fetcher, limit=2)
assert result["source"] == "hackernews"
assert result["count"] == 2
assert result["stories"][0]["score"] >= result["stories"][1]["score"]
# ---------------------------------------------------------------------------
# GitHub Trending
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_trending_repos(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.github_trending import fetch_trending_repos
respx.get(url__regex=r".*api\.github\.com/search/repositories.*").mock(
return_value=httpx.Response(200, json={
"total_count": 1,
"items": [{
"full_name": "user/cool-repo",
"description": "A cool tool",
"html_url": "https://github.com/user/cool-repo",
"stargazers_count": 500,
"forks_count": 20,
"language": "Python",
"created_at": "2026-02-20T00:00:00Z",
"topics": ["ai", "ml"],
}],
})
)
result = await fetch_trending_repos(fetcher, limit=5)
assert result["source"] == "github"
assert result["count"] == 1
assert result["repos"][0]["name"] == "user/cool-repo"
assert result["repos"][0]["stars"] == 500
# ---------------------------------------------------------------------------
# arXiv Papers
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_arxiv_papers(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.arxiv_papers import fetch_arxiv_papers
arxiv_xml = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<id>http://arxiv.org/abs/2602.12345v1</id>
<title>Attention Is Still All You Need</title>
<summary>We show transformers continue to dominate.</summary>
<author><name>Jane Doe</name></author>
<author><name>John Smith</name></author>
<published>2026-02-20T00:00:00Z</published>
<link href="http://arxiv.org/abs/2602.12345v1" rel="alternate"/>
<link href="http://arxiv.org/pdf/2602.12345v1" title="pdf" rel="related"/>
<category term="cs.AI"/>
<category term="cs.LG"/>
</entry>
</feed>"""
respx.get(url__regex=r".*export\.arxiv\.org/api/query.*").mock(
return_value=httpx.Response(200, text=arxiv_xml)
)
result = await fetch_arxiv_papers(fetcher, limit=5)
assert result["source"] == "arxiv"
assert result["count"] == 1
assert "Attention" in result["papers"][0]["title"]
assert len(result["papers"][0]["authors"]) == 2
# ---------------------------------------------------------------------------
# USA Spending
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_usa_spending(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.usa_spending import fetch_usa_spending
respx.get(url__regex=r".*api\.usaspending\.gov.*").mock(
return_value=httpx.Response(200, json={
"results": [{
"agency_name": "Department of Defense",
"abbreviation": "DOD",
"current_total_budget_authority_amount": 850000000000,
"obligated_amount": 700000000000,
"outlay_amount": 650000000000,
"agency_id": 97,
}],
"page_metadata": {"total": 1},
})
)
result = await fetch_usa_spending(fetcher, limit=5)
assert result["source"] == "usaspending"
assert result["count"] == 1
assert result["agencies"][0]["name"] == "Department of Defense"
# ---------------------------------------------------------------------------
# Environmental Events (NASA EONET)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_environmental_events(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.environmental import fetch_environmental_events
respx.get(url__regex=r".*eonet\.gsfc\.nasa\.gov.*").mock(
return_value=httpx.Response(200, json={
"events": [{
"id": "EONET_1234",
"title": "Wildfire in California",
"categories": [{"id": "wildfires", "title": "Wildfires"}],
"sources": [{"id": "InciWeb", "url": "https://inciweb.example.com"}],
"geometry": [{"date": "2026-02-20T00:00:00Z", "coordinates": [-119.5, 34.5]}],
}],
})
)
result = await fetch_environmental_events(fetcher, days=7)
assert result["source"] == "eonet"
assert result["count"] == 1
assert "Wildfire" in result["events"][0]["title"]
# ---------------------------------------------------------------------------
# Disaster Alerts (GDACS)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_disaster_alerts(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.environmental import fetch_disaster_alerts
gdacs_geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"eventtype": "EQ",
"eventname": "M6.5 Earthquake",
"alertlevel": "orange",
"alertscore": 2.5,
"severity": {"value": 6.5, "unit": "M"},
"country": "Turkey",
"fromdate": "2026-02-20T12:00:00Z",
"todate": "2026-02-20T12:05:00Z",
"url": {"report": "https://gdacs.example.com/report"},
"population": {"value": 500000},
},
"geometry": {"type": "Point", "coordinates": [29.0, 38.5]},
}],
}
respx.get(url__regex=r".*gdacs\.org.*").mock(
return_value=httpx.Response(200, json=gdacs_geojson)
)
result = await fetch_disaster_alerts(fetcher)
assert result["source"] == "gdacs"
assert result["count"] == 1
assert result["alerts"][0]["event_type"] == "EQ"
assert result["alerts"][0]["alert_level"] == "orange"
# ---------------------------------------------------------------------------
# Geospatial — Extended datasets (static, no HTTP mock needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_undersea_cables() -> None:
from world_intel_mcp.sources.geospatial import fetch_undersea_cables
result = await fetch_undersea_cables()
assert result["count"] > 0
assert result["source"] == "static-geospatial"
assert "total_length_km" in result
assert "total_capacity_tbps" in result
@pytest.mark.asyncio
async def test_fetch_ai_datacenters() -> None:
from world_intel_mcp.sources.geospatial import fetch_ai_datacenters
result = await fetch_ai_datacenters(country="USA")
assert result["count"] > 0
assert result["source"] == "static-geospatial"
assert "total_power_mw" in result
@pytest.mark.asyncio
async def test_fetch_spaceports() -> None:
from world_intel_mcp.sources.geospatial import fetch_spaceports
result = await fetch_spaceports()
assert result["count"] > 0
assert result["source"] == "static-geospatial"
@pytest.mark.asyncio
async def test_fetch_critical_minerals() -> None:
from world_intel_mcp.sources.geospatial import fetch_critical_minerals
result = await fetch_critical_minerals(mineral="lithium")
assert result["count"] > 0
assert all(d["mineral"] == "lithium" for d in result["deposits"])
@pytest.mark.asyncio
async def test_fetch_stock_exchanges() -> None:
from world_intel_mcp.sources.geospatial import fetch_stock_exchanges
result = await fetch_stock_exchanges(tier="mega")
assert result["count"] > 0
assert all(e["tier"] == "mega" for e in result["exchanges"])
assert "total_market_cap_usd_t" in result
# ---------------------------------------------------------------------------
# Country Stocks
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_country_stocks(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_country_stocks
chart_response = {
"chart": {
"result": [{
"meta": {
"symbol": "^GSPC",
"regularMarketPrice": 5200.0,
"regularMarketChangePercent": 0.75,
"currency": "USD",
}
}]
}
}
respx.get(url__regex=r".*query1\.finance\.yahoo\.com.*").mock(
return_value=httpx.Response(200, json=chart_response)
)
result = await fetch_country_stocks(fetcher, country="USA")
assert result["source"] == "yahoo-finance"
assert result["country"] == "USA"
assert "quote" in result
# ---------------------------------------------------------------------------
# Aircraft Batch
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_aircraft_details_batch(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.military import fetch_aircraft_details_batch
respx.get(url__regex=r".*hexdb\.io/api/v1/aircraft/ae1234.*").mock(
return_value=httpx.Response(200, json={
"Registration": "12-3456",
"Type": "C-17A",
"Operator": "USAF",
})
)
respx.get(url__regex=r".*hexdb\.io/api/v1/aircraft/ae5678.*").mock(
return_value=httpx.Response(200, json={
"Registration": "78-9012",
"Type": "KC-135R",
"Operator": "USAF",
})
)
result = await fetch_aircraft_details_batch(fetcher, icao24_list=["ae1234", "ae5678"])
assert result["source"] == "hexdb"
assert result["count"] == 2
assert result["requested"] == 2
# ---------------------------------------------------------------------------
# BTC Technicals (CoinGecko)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_btc_technicals(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_btc_technicals
# Generate 201 daily price points (enough for SMA-200)
prices = [[1700000000 + i * 86400, 90000 + i * 10] for i in range(201)]
respx.get("https://api.coingecko.com/api/v3/coins/bitcoin/market_chart").mock(
return_value=httpx.Response(200, json={"prices": prices})
)
result = await fetch_btc_technicals(fetcher)
assert result["source"] == "coingecko"
assert result["price"] == prices[-1][1]
assert result["sma_50"] > 0
assert result["sma_200"] > 0
assert result["mayer_multiple"] > 0
assert result["cross_signal"] in ("golden_cross", "death_cross", "neutral")
assert result["ath_distance_pct"] <= 0 # Current price <= ATH
assert result["data_points"] == 201
@respx.mock
@pytest.mark.asyncio
async def test_fetch_btc_technicals_insufficient_data(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_btc_technicals
# Only 10 data points — not enough for SMA-50
prices = [[1700000000 + i * 86400, 90000 + i * 10] for i in range(10)]
respx.get("https://api.coingecko.com/api/v3/coins/bitcoin/market_chart").mock(
return_value=httpx.Response(200, json={"prices": prices})
)
result = await fetch_btc_technicals(fetcher)
assert "error" in result
assert result["source"] == "coingecko"
# ---------------------------------------------------------------------------
# Central Bank Rates
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_central_bank_rates_no_fred(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.central_banks import fetch_central_bank_rates
import os
os.environ.pop("FRED_API_KEY", None)
result = await fetch_central_bank_rates(fetcher)
assert result["source"] == "multi"
assert result["fred_available"] is False
# Should have all 15 banks (12 curated + 3 FRED-fallback curated)
assert result["count"] == 15
# Sorted by rate descending — CBRT (45%) should be first
assert result["rates"][0]["bank"] == "Central Bank of Turkey"
assert result["rates"][0]["rate"] == 45.00
# All should be curated source
assert all(r["source"] == "curated" for r in result["rates"])
@respx.mock
@pytest.mark.asyncio
async def test_fetch_central_bank_rates_with_fred(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.central_banks import fetch_central_bank_rates
import os
os.environ["FRED_API_KEY"] = "test_key_123"
fred_response = {
"observations": [
{"date": "2026-02-25", "value": "4.33"}
]
}
respx.get("https://api.stlouisfed.org/fred/series/observations").mock(
return_value=httpx.Response(200, json=fred_response)
)
try:
result = await fetch_central_bank_rates(fetcher)
assert result["source"] == "multi"
assert result["fred_available"] is True
assert result["count"] == 15
# At least some should be from FRED
fred_sources = [r for r in result["rates"] if r["source"] == "fred"]
assert len(fred_sources) >= 1
finally:
os.environ.pop("FRED_API_KEY", None)
# ---------------------------------------------------------------------------
# USNI Fleet Tracker
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_usni_fleet(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.usni_fleet import fetch_usni_fleet
rss_xml = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>USNI News Fleet Tracker</title>
<item>
<title>USNI News Fleet and Marine Tracker: Feb. 24, 2026</title>
<link>https://news.usni.org/2026/02/24/fleet-tracker</link>
<pubDate>Mon, 24 Feb 2026 14:00:00 GMT</pubDate>
<description>
298 ships (237 USS, 61 USNS). 100 deployed (67 USS, 33 USNS). 72 underway (55 deployed, 17 local).
Carrier Strike Group 3 is currently conducting routine operations in the Western Pacific theater of operations, focused on maintaining freedom of navigation and regional security.
In the Philippine Sea, USS Abraham Lincoln (CVN-72) is conducting routine flight operations with embarked Carrier Air Wing Nine as part of a scheduled deployment to the Western Pacific region.
Meanwhile in the Mediterranean Sea near the coast of southern Europe, USS Spruance (DDG-111) is operating independently as part of standing NATO maritime forces conducting presence operations.
In the Persian Gulf near the Strait of Hormuz, Expeditionary Strike Group 7 continues its deployment supporting maritime security operations in the region.
USS Bataan (LHD-5) continues operations in the Red Sea supporting regional stability efforts and conducting routine training exercises with coalition partners.
</description>
</item>
</channel>
</rss>"""
respx.get("https://news.usni.org/category/fleet-tracker/feed").mock(
return_value=httpx.Response(200, text=rss_xml)
)
result = await fetch_usni_fleet(fetcher)
assert result["source"] == "usni-fleet-tracker"
assert result["ship_count"] >= 3 # CVN-72, DDG-111, LHD-5
assert result["report_title"] == "USNI News Fleet and Marine Tracker: Feb. 24, 2026"
# Check ships were extracted
hull_numbers = [s["hull_number"] for s in result["ships"]]
assert "CVN-72" in hull_numbers
assert "DDG-111" in hull_numbers
assert "LHD-5" in hull_numbers
# Check strike groups
sg_names = [sg["name"] for sg in result["strike_groups"]]
assert "CSG-3" in sg_names
# Check force totals extracted
assert result["force_totals"]["battle_force"]["total"] == 298
assert result["force_totals"]["deployed"]["total"] == 100
assert result["force_totals"]["underway"]["total"] == 72
# Check region classification (±200 char context windows may overlap in short text,
# but at least some ships should get classified to a known COCOM region)
regions = set(s["region"] for s in result["ships"])
assert len(regions - {"UNKNOWN"}) > 0 # At least one classified
@respx.mock
@pytest.mark.asyncio
async def test_fetch_usni_fleet_empty_feed(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.usni_fleet import fetch_usni_fleet
rss_xml = """<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>Empty</title></channel></rss>"""
respx.get("https://news.usni.org/category/fleet-tracker/feed").mock(
return_value=httpx.Response(200, text=rss_xml)
)
result = await fetch_usni_fleet(fetcher)
assert result["source"] == "usni-fleet-tracker"
assert "error" in result
assert result["ship_count"] == 0
# ---------------------------------------------------------------------------
# Trade Routes (static — no HTTP mock needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_trade_routes() -> None:
from world_intel_mcp.sources.geospatial import fetch_trade_routes
result = await fetch_trade_routes()
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert result["total_oil_flow_mbd"] > 0
assert "by_type" in result
assert "chokepoint" in result["by_type"]
@pytest.mark.asyncio
async def test_fetch_trade_routes_filter_type() -> None:
from world_intel_mcp.sources.geospatial import fetch_trade_routes
result = await fetch_trade_routes(route_type="canal")
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert all(r["type"] == "canal" for r in result["routes"])
@pytest.mark.asyncio
async def test_fetch_trade_routes_filter_country() -> None:
from world_intel_mcp.sources.geospatial import fetch_trade_routes
result = await fetch_trade_routes(country="EGY")
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert all("EGY" in r["countries"] for r in result["routes"])
# ---------------------------------------------------------------------------
# Cloud Regions (static — no HTTP mock needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_cloud_regions() -> None:
from world_intel_mcp.sources.geospatial import fetch_cloud_regions
result = await fetch_cloud_regions()
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert "by_provider" in result
assert "AWS" in result["by_provider"]
@pytest.mark.asyncio
async def test_fetch_cloud_regions_filter_provider() -> None:
from world_intel_mcp.sources.geospatial import fetch_cloud_regions
result = await fetch_cloud_regions(provider="GCP")
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert all(r["provider"] == "GCP" for r in result["regions"])
# ---------------------------------------------------------------------------
# Financial Centers (static — no HTTP mock needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_financial_centers() -> None:
from world_intel_mcp.sources.geospatial import fetch_financial_centers
result = await fetch_financial_centers()
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert "by_country" in result
@pytest.mark.asyncio
async def test_fetch_financial_centers_filter_rank() -> None:
from world_intel_mcp.sources.geospatial import fetch_financial_centers
result = await fetch_financial_centers(min_rank=5)
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert result["count"] <= 5
assert all(fc["gfci_rank"] <= 5 for fc in result["centers"])
@pytest.mark.asyncio
async def test_fetch_financial_centers_filter_country() -> None:
from world_intel_mcp.sources.geospatial import fetch_financial_centers
result = await fetch_financial_centers(country="USA")
assert result["source"] == "static-geospatial"
assert result["count"] > 0
assert all(fc["iso3"] == "USA" for fc in result["centers"])
# ---------------------------------------------------------------------------
# Country Dossier (Phase 16)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_fetch_country_dossier(fetcher: Fetcher) -> None:
"""Dossier aggregates country brief, stocks, elections, sanctions, news."""
from world_intel_mcp.analysis.dossier import fetch_country_dossier
# Mock World Bank GDP
respx.get("https://api.worldbank.org/v2/country/US/indicator/NY.GDP.MKTP.CD").mock(
return_value=httpx.Response(200, json=[
{"page": 1},
[{"date": "2024", "value": 28000000000000}],
])
)
# Mock World Bank inflation
respx.get("https://api.worldbank.org/v2/country/US/indicator/FP.CPI.TOTL.ZG").mock(
return_value=httpx.Response(200, json=[
{"page": 1},
[{"date": "2024", "value": 3.2}],
])
)
# Mock ACLED (no key = skip)
# Mock Yahoo Finance for country stocks
respx.get("https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC").mock(
return_value=httpx.Response(200, json={
"chart": {"result": [{"meta": {
"regularMarketPrice": 5800,
"chartPreviousClose": 5750,
"currency": "USD",
"exchangeName": "SNP",
}}]},
})
)
# Mock OFAC sanctions
respx.get("https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML").mock(
return_value=httpx.Response(200, text="<sdnList></sdnList>")
)
# Mock news feeds — just one category needed
respx.route().mock(return_value=httpx.Response(200, text="""<?xml version="1.0"?>
<rss version="2.0"><channel><title>Test</title>
<item><title>US Economy Grows</title><link>https://example.com/1</link></item>
<item><title>China Trade</title><link>https://example.com/2</link></item>
</channel></rss>"""))
result = await fetch_country_dossier(fetcher, country="US")
assert result["source"] == "country-dossier"
assert result["overview"]["iso2"] == "US"
assert result["overview"]["iso3"] == "USA"
assert "economy" in result
assert "markets" in result
assert "elections" in result
assert "sanctions" in result
assert "news" in result
assert "security" in result
assert len(result["sections"]) == 7
@pytest.mark.asyncio
async def test_fetch_country_dossier_invalid_code(fetcher: Fetcher) -> None:
"""Dossier returns error for unknown country code."""
from world_intel_mcp.analysis.dossier import fetch_country_dossier
result = await fetch_country_dossier(fetcher, country="XX")
assert "error" in result
assert "Unknown" in result["error"]
# ---------------------------------------------------------------------------
# Traffic (Phase 16)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_fetch_traffic_flow_no_key(fetcher: Fetcher) -> None:
"""Traffic flow returns error when TOMTOM_API_KEY is not set."""
from world_intel_mcp.sources.traffic import fetch_traffic_flow
with patch.dict("os.environ", {}, clear=True):
result = await fetch_traffic_flow(fetcher)
assert "error" in result
assert "TOMTOM_API_KEY" in result["error"]
@pytest.mark.asyncio
@respx.mock
async def test_fetch_traffic_flow_with_key(fetcher: Fetcher) -> None:
"""Traffic flow fetches congestion data from TomTom."""
from world_intel_mcp.sources.traffic import fetch_traffic_flow
respx.route().mock(return_value=httpx.Response(200, json={
"flowSegmentData": {
"currentSpeed": 30.0,
"freeFlowSpeed": 60.0,
},
}))
with patch.dict("os.environ", {"TOMTOM_API_KEY": "test-key"}):
result = await fetch_traffic_flow(fetcher)
assert result["source"] == "tomtom"
assert result["count"] > 0
assert result["global_avg_congestion"] == 50.0 # (1 - 30/60) * 100
assert result["cities"][0]["congestion_pct"] == 50
@pytest.mark.asyncio
@respx.mock
async def test_fetch_traffic_incidents_no_key(fetcher: Fetcher) -> None:
"""Traffic incidents returns error when no API key."""
from world_intel_mcp.sources.traffic import fetch_traffic_incidents
with patch.dict("os.environ", {}, clear=True):
result = await fetch_traffic_incidents(fetcher)
assert "error" in result
# ---------------------------------------------------------------------------
# Aviation Domestic (Phase 16)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@respx.mock
async def test_fetch_domestic_flights(fetcher: Fetcher) -> None:
"""Domestic flights buckets OpenSky states by region."""
from world_intel_mcp.sources.aviation import fetch_domestic_flights
# Simulate 3 airborne aircraft at known positions
respx.route().mock(return_value=httpx.Response(200, json={
"states": [
# [icao24, callsign, origin, ..., on_ground=False, lon, lat, ...]
["abc123", "UAL123 ", "United States", None, None, -73.9, 40.7, 10000, False, None, None, None, None, None, None, None],
["def456", "BAW789 ", "United Kingdom", None, None, -0.1, 51.5, 11000, False, None, None, None, None, None, None, None],
["ghi789", "CCA100 ", "China", None, None, 116.4, 39.9, 12000, False, None, None, None, None, None, None, None],
],
}))
result = await fetch_domestic_flights(fetcher)
assert result["source"] == "opensky-domestic"
assert result["total_aircraft"] == 3
assert len(result["by_region"]) > 0
assert len(result["busiest_origins"]) > 0
# ---------------------------------------------------------------------------
# Webcams (Phase 16)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_webcams_no_key(fetcher: Fetcher) -> None:
"""Webcams returns error when WINDY_API_KEY is not set."""
from world_intel_mcp.sources.webcams import fetch_webcams
with patch.dict("os.environ", {}, clear=True):
result = await fetch_webcams(fetcher)
assert "error" in result
assert "WINDY_API_KEY" in result["error"]
@pytest.mark.asyncio
@respx.mock
async def test_fetch_webcams_with_key(fetcher: Fetcher) -> None:
"""Webcams fetches camera data from Windy API."""
from world_intel_mcp.sources.webcams import fetch_webcams
respx.route().mock(return_value=httpx.Response(200, json={
"webcams": [
{
"webcamId": "cam-1",
"title": "Times Square",
"location": {"latitude": 40.758, "longitude": -73.985, "city": "New York", "country": "US"},
"images": {"current": {"preview": "https://example.com/prev.jpg", "thumbnail": "https://example.com/thumb.jpg"}},
"player": {"day": {"embed": "https://example.com/player"}},
"status": "active",
},
],
}))
with patch.dict("os.environ", {"WINDY_API_KEY": "test-key"}):
result = await fetch_webcams(fetcher, category="traffic", limit=10)
assert result["source"] == "windy-webcams"
assert result["count"] == 1
assert result["cameras"][0]["title"] == "Times Square"
assert result["cameras"][0]["lat"] == 40.758