"""Local SDE store: offline lookups and SDE-preferred / ESI-fallback wiring.""" import asyncio import bz2 import sqlite3 from pathlib import Path import httpx import pytest from familiar import sde, universe def _make_sde(rows=((34, "Tritanium", 18, 0.01), (35, "Pyerite", 18, 0.01))): """In-memory SQLite seeded with the minimal invTypes schema and rows.""" conn = sqlite3.connect(":memory:") conn.execute( "CREATE TABLE invTypes (" "typeID INTEGER PRIMARY KEY, typeName TEXT, groupID INTEGER, volume REAL)" ) conn.executemany("INSERT INTO invTypes VALUES (?, ?, ?, ?)", rows) conn.commit() return conn def _build_sde_file(path: Path) -> None: """Write a tiny on-disk SDE with invTypes, mapSolarSystems and staStations.""" conn = sqlite3.connect(str(path)) conn.executescript( "CREATE TABLE invTypes (" "typeID INTEGER PRIMARY KEY, typeName TEXT, groupID INTEGER, volume REAL);" "CREATE TABLE mapSolarSystems (" "solarSystemID INTEGER PRIMARY KEY, solarSystemName TEXT, regionID INTEGER, " "constellationID INTEGER, security REAL);" "CREATE TABLE staStations (" "stationID INTEGER PRIMARY KEY, stationName TEXT, solarSystemID INTEGER, " "corporationID INTEGER);" ) conn.execute("INSERT INTO invTypes VALUES (?, ?, ?, ?)", (34, "Tritanium", 18, 0.01)) conn.execute( "INSERT INTO mapSolarSystems VALUES (?, ?, ?, ?, ?)", (30000142, "Jita", 10000002, 20000020, 0.945913), ) conn.execute( "INSERT INTO staStations VALUES (?, ?, ?, ?)", (60003760, "Jita IV - Moon 4 - Caldari Navy Assembly Plant", 30000142, 1000035), ) conn.commit() conn.close() def _make_sde_full(): """In-memory SQLite with invTypes, mapSolarSystems and staStations populated.""" conn = sqlite3.connect(":memory:") conn.executescript( "CREATE TABLE invTypes (" "typeID INTEGER PRIMARY KEY, typeName TEXT, groupID INTEGER, volume REAL);" "CREATE TABLE mapSolarSystems (" "solarSystemID INTEGER PRIMARY KEY, solarSystemName TEXT, regionID INTEGER, " "constellationID INTEGER, security REAL);" "CREATE TABLE staStations (" "stationID INTEGER PRIMARY KEY, stationName TEXT, solarSystemID INTEGER, " "corporationID INTEGER);" ) conn.execute("INSERT INTO invTypes VALUES (?, ?, ?, ?)", (34, "Tritanium", 18, 0.01)) conn.execute( "INSERT INTO mapSolarSystems VALUES (?, ?, ?, ?, ?)", (30000142, "Jita", 10000002, 20000020, 0.945913), ) conn.execute( "INSERT INTO staStations VALUES (?, ?, ?, ?)", (60003760, "Jita IV - Moon 4 - CNAP", 30000142, 1000035), ) conn.commit() return conn @pytest.fixture def use_sde(): """Inject an in-memory SDE and reset the cached connection afterwards.""" conn = _make_sde() sde.set_connection(conn) yield conn sde.set_connection(None) conn.close() @pytest.fixture def use_sde_full(): """Inject an in-memory SDE with systems + stations; reset afterwards.""" conn = _make_sde_full() sde.set_connection(conn) yield conn sde.set_connection(None) conn.close() def test_available_true_with_table(use_sde): assert sde.available() is True def test_available_false_without_connection(): sde.set_connection(None) # No file on the default/overridden path in the test environment. assert sde.available() is False def test_available_false_when_table_missing(): conn = sqlite3.connect(":memory:") # connected, but no invTypes table sde.set_connection(conn) try: assert sde.available() is False # The graceful-degradation accessors must not crash either. assert sde.type_by_id(34) is None assert sde.type_by_name("Tritanium") is None assert sde.resolve_type_name("Tritanium") is None finally: sde.set_connection(None) conn.close() def test_type_by_id(use_sde): item = sde.type_by_id(34) assert item == {"type_id": 34, "name": "Tritanium", "group_id": 18, "volume": 0.01} def test_type_by_id_missing(use_sde): assert sde.type_by_id(999999) is None def test_type_by_name(use_sde): item = sde.type_by_name("Pyerite") assert item["type_id"] == 35 def test_resolve_type_name_case_insensitive(use_sde): assert sde.resolve_type_name("tritanium") == 34 assert sde.resolve_type_name("TRITANIUM") == 34 assert sde.resolve_type_name("Tritanium") == 34 def test_resolve_type_name_unknown(use_sde): assert sde.resolve_type_name("Nope") is None def test_sde_status(use_sde): status = sde.sde_status() assert status["available"] is True assert isinstance(status["path"], str) def test_get_item_uses_sde_without_esi(use_sde, mock_esi): def boom(request): # any ESI call here is a bug raise AssertionError(f"ESI was called: {request.url}") mock_esi(boom) result = asyncio.run(universe.get_item("Tritanium")) assert result["type_id"] == 34 assert result["name"] == "Tritanium" assert result["volume"] == 0.01 # SDE has no description / packaged_volume. assert result["description"] is None assert result["packaged_volume"] is None def test_get_item_falls_back_to_esi_when_sde_absent(mock_esi): sde.set_connection(None) # no SDE fixtures = { "/universe/ids/": {"inventory_types": [{"id": 34, "name": "Tritanium"}]}, "/universe/types/": { "type_id": 34, "name": "Tritanium", "group_id": 18, "volume": 0.01, "packaged_volume": 0.01, "description": "The most common ore type.", }, } def route(request): for needle, body in fixtures.items(): if needle in request.url.path: return httpx.Response(200, json=body) return httpx.Response(404, json={"error": "no fixture"}) mock_esi(route) result = asyncio.run(universe.get_item("Tritanium")) assert result["type_id"] == 34 assert result["description"] == "The most common ore type." # --- system / station offline lookups ------------------------------------- def test_system_by_id(use_sde_full): sysrow = sde.system_by_id(30000142) assert sysrow == { "system_id": 30000142, "name": "Jita", "region_id": 10000002, "constellation_id": 20000020, "security_status": 0.945913, } def test_system_by_name_case_insensitive(use_sde_full): assert sde.system_by_name("jita")["system_id"] == 30000142 assert sde.system_by_name("JITA")["system_id"] == 30000142 def test_system_lookups_missing(use_sde_full): assert sde.system_by_id(999999) is None assert sde.system_by_name("Nowhere") is None def test_station_by_id(use_sde_full): station = sde.station_by_id(60003760) assert station == { "station_id": 60003760, "name": "Jita IV - Moon 4 - CNAP", "system_id": 30000142, } def test_station_by_id_missing(use_sde_full): assert sde.station_by_id(999999) is None def test_system_station_lookups_degrade_without_table(use_sde): # `use_sde` has only invTypes — the system/station tables are absent. assert sde.system_by_id(30000142) is None assert sde.system_by_name("Jita") is None assert sde.station_by_id(60003760) is None def test_system_station_lookups_degrade_without_connection(): sde.set_connection(None) assert sde.system_by_id(30000142) is None assert sde.system_by_name("Jita") is None assert sde.station_by_id(60003760) is None # --- universe SDE fast-path / ESI fallback -------------------------------- def test_get_system_uses_sde_without_esi(use_sde_full, mock_esi): def boom(request): # any ESI call here is a bug raise AssertionError(f"ESI was called: {request.url}") mock_esi(boom) result = asyncio.run(universe.get_system("Jita")) assert result["system_id"] == 30000142 assert result["name"] == "Jita" assert result["constellation_id"] == 20000020 assert result["security_status"] == 0.95 # rounded to 2 dp assert result["security_class"] is None assert result["planets"] == 0 assert result["stargates"] == 0 def test_get_system_falls_back_to_esi_when_sde_absent(mock_esi): sde.set_connection(None) fixtures = { "/universe/ids/": {"systems": [{"id": 30000142, "name": "Jita"}]}, "/universe/systems/": { "system_id": 30000142, "name": "Jita", "constellation_id": 20000020, "security_status": 0.9459, "security_class": "B", "planets": [{"planet_id": 1}], "stargates": [1, 2, 3], }, } def route(request): for needle, body in fixtures.items(): if needle in request.url.path: return httpx.Response(200, json=body) return httpx.Response(404, json={"error": "no fixture"}) mock_esi(route) result = asyncio.run(universe.get_system("Jita")) assert result["system_id"] == 30000142 assert result["security_status"] == 0.95 assert result["security_class"] == "B" assert result["planets"] == 1 assert result["stargates"] == 3 def test_get_station_uses_sde_for_numeric_id_without_esi(use_sde_full, mock_esi): def boom(request): raise AssertionError(f"ESI was called: {request.url}") mock_esi(boom) result = asyncio.run(universe.get_station(60003760)) assert result["station_id"] == 60003760 assert result["system_id"] == 30000142 assert result["type_id"] is None assert result["services"] is None def test_get_station_falls_back_to_esi_when_sde_absent(mock_esi): sde.set_connection(None) fixtures = { "/universe/stations/": { "station_id": 60003760, "name": "Jita IV - Moon 4 - CNAP", "system_id": 30000142, "type_id": 1529, "services": ["market", "repair"], }, } def route(request): for needle, body in fixtures.items(): if needle in request.url.path: return httpx.Response(200, json=body) return httpx.Response(404, json={"error": "no fixture"}) mock_esi(route) result = asyncio.run(universe.get_station(60003760)) assert result["type_id"] == 1529 assert result["services"] == ["market", "repair"] # --- download_sde ---------------------------------------------------------- def test_download_sde_decompresses_and_installs(tmp_path): src = tmp_path / "source.sqlite" _build_sde_file(src) compressed = bz2.compress(src.read_bytes()) def handler(request): # Serve the bz2 bytes in a couple of chunks to exercise streaming. return httpx.Response(200, content=compressed) dest = tmp_path / "installed.sqlite" try: result = asyncio.run( sde.download_sde( url="https://example.invalid/sde.sqlite.bz2", dest=dest, transport=httpx.MockTransport(handler), ) ) assert result["path"] == str(dest) assert dest.is_file() assert result["bytes"] == dest.stat().st_size # Point the live store at the freshly written file and query it. import os os.environ["FAMILIAR_SDE_PATH"] = str(dest) sde.set_connection(None) try: assert sde.available() is True assert sde.type_by_id(34)["name"] == "Tritanium" assert sde.system_by_name("Jita")["system_id"] == 30000142 finally: del os.environ["FAMILIAR_SDE_PATH"] sde.set_connection(None) finally: sde.set_connection(None)