mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Map viewer has simple search. Map search databases can be downloaded from CDN, or built locally. Map searches can be built offline, but will not have enriched things like state names downloaded from Wikidata.
166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
import sqlite3
|
|
from http import HTTPStatus
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
|
|
def _create_test_search_db(path: Path, features: list):
|
|
"""Create a minimal .search.db file with given features for testing."""
|
|
conn = sqlite3.connect(str(path))
|
|
conn.execute('PRAGMA journal_mode=WAL')
|
|
conn.execute("""
|
|
CREATE TABLE places (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
kind TEXT,
|
|
lat REAL NOT NULL,
|
|
lon REAL NOT NULL,
|
|
min_zoom REAL,
|
|
source TEXT,
|
|
kind_detail TEXT,
|
|
population INTEGER,
|
|
wikidata TEXT,
|
|
region TEXT
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE VIRTUAL TABLE places_fts USING fts5(
|
|
name, kind, content=places, content_rowid=id
|
|
)
|
|
""")
|
|
conn.executemany(
|
|
'INSERT INTO places (name, kind, lat, lon, min_zoom, source, kind_detail, population, region)'
|
|
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
features,
|
|
)
|
|
conn.execute("INSERT INTO places_fts(places_fts) VALUES('rebuild')")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def search_db(test_directory):
|
|
"""Create a test search database with known features."""
|
|
map_dir = test_directory / 'map'
|
|
map_dir.mkdir(exist_ok=True)
|
|
|
|
features = [
|
|
('Portland', 'locality', 45.5217, -122.6733, 4.0, 'test', 'city', 652503, 'Oregon'),
|
|
('Portland', 'locality', 43.6559, -70.2576, 5.0, 'test', 'city', 78408, 'Maine'),
|
|
('San Francisco', 'locality', 37.7881, -122.4097, 3.0, 'test', 'city', 873965, 'California'),
|
|
('Springfield', 'locality', 39.7990, -89.6436, 6.0, 'test', 'city', 114230, 'Illinois'),
|
|
('Springfield', 'locality', 37.2085, -93.2918, 6.0, 'test', 'city', 169176, 'Missouri'),
|
|
('Yellowstone National Park', 'nature_reserve', 44.6222, -110.5616, 8.0, 'test', '', None, None),
|
|
]
|
|
db_path = map_dir / 'test.search.db'
|
|
_create_test_search_db(db_path, features)
|
|
return db_path
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_requires_query(async_client, test_directory):
|
|
"""Search endpoint requires a query parameter."""
|
|
request, response = await async_client.get('/api/map/search')
|
|
assert response.status_code == HTTPStatus.BAD_REQUEST
|
|
assert 'error' in response.json
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_empty_query(async_client, test_directory):
|
|
"""Search endpoint rejects empty query."""
|
|
request, response = await async_client.get('/api/map/search?q=')
|
|
assert response.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_no_indexes(async_client, test_directory):
|
|
"""Search returns empty results when no search indexes exist."""
|
|
request, response = await async_client.get('/api/map/search?q=portland')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['results'] == []
|
|
assert response.json['total'] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_returns_results(async_client, test_directory, search_db):
|
|
"""Search returns matching results from the index."""
|
|
request, response = await async_client.get('/api/map/search?q=Portland')
|
|
assert response.status_code == HTTPStatus.OK
|
|
results = response.json['results']
|
|
assert len(results) >= 1
|
|
assert response.json['total'] >= 1
|
|
assert results[0]['name'] == 'Portland'
|
|
assert 'lat' in results[0]
|
|
assert 'lon' in results[0]
|
|
assert 'kind' in results[0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_with_coordinates(async_client, test_directory, search_db):
|
|
"""Search with lat/lon returns results sorted by proximity."""
|
|
# Bias toward Portland, OR (45.5, -122.6)
|
|
request, response = await async_client.get('/api/map/search?q=Portland&lat=45.5&lon=-122.6')
|
|
assert response.status_code == HTTPStatus.OK
|
|
results = response.json['results']
|
|
assert len(results) >= 2
|
|
# Portland, OR should be first (closest to bias point).
|
|
assert results[0]['lat'] == pytest.approx(45.5217, abs=0.1)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_prefix_matching(async_client, test_directory, search_db):
|
|
"""Search supports prefix matching."""
|
|
request, response = await async_client.get('/api/map/search?q=Spring')
|
|
assert response.status_code == HTTPStatus.OK
|
|
results = response.json['results']
|
|
assert any(r['name'] == 'Springfield' for r in results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_status_no_files(async_client, test_directory):
|
|
"""Status endpoint returns empty when no map files exist."""
|
|
request, response = await async_client.get('/api/map/search/status')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['indexed'] == []
|
|
assert response.json['missing'] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_status_with_index(async_client, test_directory, search_db, make_files_structure):
|
|
"""Status endpoint shows which files have indexes."""
|
|
# Create a PMTiles file that matches the search.db stem.
|
|
make_files_structure(['map/test.pmtiles'])
|
|
|
|
request, response = await async_client.get('/api/map/search/status')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert len(response.json['indexed']) == 1
|
|
assert response.json['indexed'][0]['stem'] == 'test'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_status_missing_index(async_client, test_directory, make_files_structure):
|
|
"""Status endpoint shows PMTiles files without search indexes."""
|
|
make_files_structure(['map/usa.pmtiles'])
|
|
|
|
request, response = await async_client.get('/api/map/search/status')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert len(response.json['missing']) == 1
|
|
assert response.json['missing'][0]['stem'] == 'usa'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_pmtiles_also_deletes_search_db(async_client, test_directory, search_db, make_files_structure):
|
|
"""Deleting a PMTiles file also deletes its companion search.db."""
|
|
make_files_structure(['map/test.pmtiles'])
|
|
|
|
# Both files exist.
|
|
assert search_db.exists()
|
|
|
|
# Delete the PMTiles file.
|
|
request, response = await async_client.delete('/api/map/files/test.pmtiles')
|
|
assert response.status_code == HTTPStatus.NO_CONTENT
|
|
|
|
# Search DB should also be deleted.
|
|
assert not search_db.exists()
|