mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-06-11 00:34:54 +02:00
560eb0796a
Replace the role=infra NodeTag convention with UserProfileNode adoption as the canonical infrastructure indicator across map, Prometheus metrics, and alerting. Renames is_infra to is_adopted, infra_center to adopted_center. Map icons change to blue (adopted) / green (normal), with all adoption UI gated on OIDC_ENABLED. Adds meshcore_nodes_adopted gauge and Alembic migration to clean up obsolete tags.
465 lines
16 KiB
Python
465 lines
16 KiB
Python
"""Tests for the map page routes."""
|
|
|
|
from typing import Any
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from tests.test_web.conftest import MockHttpClient
|
|
|
|
|
|
class TestMapPage:
|
|
"""Tests for the map page."""
|
|
|
|
def test_map_returns_200(self, client: TestClient) -> None:
|
|
"""Test that map page returns 200 status code."""
|
|
response = client.get("/map")
|
|
assert response.status_code == 200
|
|
|
|
def test_map_returns_html(self, client: TestClient) -> None:
|
|
"""Test that map page returns HTML content."""
|
|
response = client.get("/map")
|
|
assert "text/html" in response.headers["content-type"]
|
|
|
|
def test_map_contains_network_name(self, client: TestClient) -> None:
|
|
"""Test that map page contains the network name."""
|
|
response = client.get("/map")
|
|
assert "Test Network" in response.text
|
|
|
|
def test_map_contains_leaflet(self, client: TestClient) -> None:
|
|
"""Test that map page includes Leaflet library."""
|
|
response = client.get("/map")
|
|
# Should include Leaflet JS/CSS
|
|
assert "leaflet" in response.text.lower()
|
|
|
|
|
|
class TestMapDataEndpoint:
|
|
"""Tests for the map data JSON endpoint."""
|
|
|
|
def test_map_data_returns_200(self, client: TestClient) -> None:
|
|
"""Test that map data endpoint returns 200 status code."""
|
|
response = client.get("/map/data")
|
|
assert response.status_code == 200
|
|
|
|
def test_map_data_returns_json(self, client: TestClient) -> None:
|
|
"""Test that map data endpoint returns JSON content."""
|
|
response = client.get("/map/data")
|
|
assert "application/json" in response.headers["content-type"]
|
|
|
|
def test_map_data_contains_nodes(
|
|
self, client: TestClient, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data contains nodes with location."""
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
assert "nodes" in data
|
|
# The mock includes a node with lat/lon tags
|
|
nodes = data["nodes"]
|
|
# Should have at least one node with location
|
|
assert len(nodes) == 1
|
|
assert nodes[0]["name"] == "Node Two"
|
|
assert nodes[0]["lat"] == 40.7128
|
|
assert nodes[0]["lon"] == -74.0060
|
|
|
|
def test_map_data_contains_center(
|
|
self, client: TestClient, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data contains network center location."""
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
assert "center" in data
|
|
center = data["center"]
|
|
assert center["lat"] == 40.7128
|
|
assert center["lon"] == -74.0060
|
|
|
|
def test_map_data_excludes_nodes_without_location(
|
|
self, client: TestClient, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data excludes nodes without location tags."""
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
nodes = data["nodes"]
|
|
# Node One has no location tags, so should not appear
|
|
node_names = [n["name"] for n in nodes]
|
|
assert "Node One" not in node_names
|
|
|
|
|
|
class TestMapDataAPIErrors:
|
|
"""Tests for map data handling API errors."""
|
|
|
|
def test_map_data_handles_api_error(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data handles API errors gracefully."""
|
|
mock_http_client.set_response(
|
|
"GET", "/api/v1/nodes", status_code=500, json_data=None
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
|
|
# Should still return 200 with empty nodes
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["nodes"] == []
|
|
assert "center" in data
|
|
|
|
|
|
class TestMapDataFiltering:
|
|
"""Tests for map data location filtering."""
|
|
|
|
def test_map_data_filters_invalid_lat(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data filters nodes with invalid latitude."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Bad Lat Node",
|
|
"tags": [
|
|
{"key": "lat", "value": "not-a-number"},
|
|
{"key": "lon", "value": "-74.0060"},
|
|
],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
# Node with invalid lat should be excluded
|
|
assert len(data["nodes"]) == 0
|
|
|
|
def test_map_data_filters_missing_lon(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data filters nodes with missing longitude."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "No Lon Node",
|
|
"tags": [
|
|
{"key": "lat", "value": "40.7128"},
|
|
],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
# Node with only lat should be excluded
|
|
assert len(data["nodes"]) == 0
|
|
|
|
def test_map_data_filters_zero_coordinates(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data filters nodes with (0, 0) coordinates."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Zero Coord Node",
|
|
"lat": 0.0,
|
|
"lon": 0.0,
|
|
"tags": [],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
# Node at (0, 0) should be excluded
|
|
assert len(data["nodes"]) == 0
|
|
|
|
def test_map_data_uses_model_coordinates_as_fallback(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data uses model lat/lon when tags are not present."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Model Coords Node",
|
|
"lat": 51.5074,
|
|
"lon": -0.1278,
|
|
"tags": [],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
# Node should use model coordinates
|
|
assert len(data["nodes"]) == 1
|
|
assert data["nodes"][0]["lat"] == 51.5074
|
|
assert data["nodes"][0]["lon"] == -0.1278
|
|
|
|
def test_map_data_prefers_tag_coordinates_over_model(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that tag coordinates take priority over model coordinates."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Both Coords Node",
|
|
"lat": 51.5074,
|
|
"lon": -0.1278,
|
|
"tags": [
|
|
{"key": "lat", "value": "40.7128"},
|
|
{"key": "lon", "value": "-74.0060"},
|
|
],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
# Node should use tag coordinates, not model
|
|
assert len(data["nodes"]) == 1
|
|
assert data["nodes"][0]["lat"] == 40.7128
|
|
assert data["nodes"][0]["lon"] == -74.0060
|
|
|
|
|
|
class TestMapDataAdoptedNodes:
|
|
"""Tests for adopted node handling in map data."""
|
|
|
|
def test_map_data_includes_adopted_center(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data includes adopted center when adopted nodes exist."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Adopted Node",
|
|
"lat": 40.0,
|
|
"lon": -74.0,
|
|
"tags": [],
|
|
"adopted_by": {
|
|
"user_id": "user-1",
|
|
"name": "Operator",
|
|
"callsign": "W1ABC",
|
|
"profile_id": "profile-1",
|
|
},
|
|
},
|
|
{
|
|
"id": "node-2",
|
|
"public_key": "def456",
|
|
"name": "Regular Node",
|
|
"lat": 41.0,
|
|
"lon": -75.0,
|
|
"tags": [],
|
|
},
|
|
],
|
|
"total": 2,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
assert data["adopted_center"] is not None
|
|
assert data["adopted_center"]["lat"] == 40.0
|
|
assert data["adopted_center"]["lon"] == -74.0
|
|
|
|
def test_map_data_adopted_center_null_when_no_adopted(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that adopted_center is null when no adopted nodes exist."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Regular Node",
|
|
"lat": 40.0,
|
|
"lon": -74.0,
|
|
"tags": [],
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
assert data["adopted_center"] is None
|
|
|
|
def test_map_data_sets_is_adopted_flag(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that nodes have correct is_adopted flag based on adoption."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Adopted Node",
|
|
"lat": 40.0,
|
|
"lon": -74.0,
|
|
"tags": [],
|
|
"adopted_by": {
|
|
"user_id": "user-1",
|
|
"name": "Operator",
|
|
"callsign": "W1ABC",
|
|
"profile_id": "profile-1",
|
|
},
|
|
},
|
|
{
|
|
"id": "node-2",
|
|
"public_key": "def456",
|
|
"name": "Regular Node",
|
|
"lat": 41.0,
|
|
"lon": -75.0,
|
|
"tags": [],
|
|
},
|
|
],
|
|
"total": 2,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
nodes_by_name = {n["name"]: n for n in data["nodes"]}
|
|
assert nodes_by_name["Adopted Node"]["is_adopted"] is True
|
|
assert nodes_by_name["Regular Node"]["is_adopted"] is False
|
|
|
|
def test_map_data_debug_includes_adopted_count(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that debug info includes adopted node count."""
|
|
mock_http_client.set_response(
|
|
"GET",
|
|
"/api/v1/nodes",
|
|
status_code=200,
|
|
json_data={
|
|
"items": [
|
|
{
|
|
"id": "node-1",
|
|
"public_key": "abc123",
|
|
"name": "Adopted Node",
|
|
"lat": 40.0,
|
|
"lon": -74.0,
|
|
"tags": [],
|
|
"adopted_by": {
|
|
"user_id": "user-1",
|
|
"name": "Operator",
|
|
"callsign": "W1ABC",
|
|
"profile_id": "profile-1",
|
|
},
|
|
},
|
|
],
|
|
"total": 1,
|
|
},
|
|
)
|
|
web_app.state.http_client = mock_http_client
|
|
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
data = response.json()
|
|
|
|
assert data["debug"]["adopted_nodes"] == 1
|
|
|
|
|
|
class TestMapDataAdoptedByFilter:
|
|
"""Tests for map data adopted_by filter parameter."""
|
|
|
|
def test_map_data_accepts_adopted_by_param(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data endpoint accepts adopted_by query parameter."""
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data?adopted_by=some-profile-uuid")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "nodes" in data
|
|
assert "profiles" in data
|
|
|
|
def test_map_data_adopted_by_empty_returns_all(
|
|
self, web_app: Any, mock_http_client: MockHttpClient
|
|
) -> None:
|
|
"""Test that map data without adopted_by returns nodes normally."""
|
|
client = TestClient(web_app, raise_server_exceptions=True)
|
|
response = client.get("/map/data")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Default mock has 2 nodes, 1 with coordinates
|
|
assert len(data["nodes"]) == 1
|
|
assert data["nodes"][0]["name"] == "Node Two"
|