Add web dashboard tests for Phase 5.11

- Create conftest.py with MockHttpClient for testing web routes
- Add test_home.py with 9 tests for home page
- Add test_members.py with 11 tests for members page and load_members function
- Add test_network.py with 7 tests for network overview page
- Add test_nodes.py with 15 tests for nodes list and detail pages
- Add test_map.py with 12 tests for map page and data endpoint
- Add test_messages.py with 13 tests for messages page with filtering
- All 67 web tests pass, 184 total tests pass
- Update TASKS.md to mark Phase 5 as 100% complete (186/221 total)
This commit is contained in:
Claude
2025-12-03 15:06:40 +00:00
parent a98de503c7
commit 65c77afbe0
9 changed files with 1025 additions and 12 deletions

View File

@@ -2,4 +2,4 @@
Refer to @AGENTS.md for coding instructions.
DO NOT MODIFY "CLAUDE.md", UPDATE "AGENTS.md".
DO NOT MODIFY "CLAUDE.md", UPDATE "AGENTS.md".

View File

@@ -602,15 +602,15 @@ This document tracks implementation progress for the MeshCore Hub project. Each
### 5.11 Web Tests
- [ ] Create `tests/test_web/conftest.py`:
- [ ] Test client fixture
- [ ] Mock API responses
- [ ] Create `tests/test_web/test_home.py`
- [ ] Create `tests/test_web/test_members.py`
- [ ] Create `tests/test_web/test_network.py`
- [ ] Create `tests/test_web/test_nodes.py`
- [ ] Create `tests/test_web/test_map.py`
- [ ] Create `tests/test_web/test_messages.py`
- [x] Create `tests/test_web/conftest.py`:
- [x] Test client fixture
- [x] Mock API responses
- [x] Create `tests/test_web/test_home.py`
- [x] Create `tests/test_web/test_members.py`
- [x] Create `tests/test_web/test_network.py`
- [x] Create `tests/test_web/test_nodes.py`
- [x] Create `tests/test_web/test_map.py`
- [x] Create `tests/test_web/test_messages.py`
---
@@ -740,9 +740,9 @@ This document tracks implementation progress for the MeshCore Hub project. Each
| Phase 2: Interface | 35 | 35 | 100% |
| Phase 3: Collector | 27 | 20 | 74% |
| Phase 4: API | 44 | 44 | 100% |
| Phase 5: Web Dashboard | 40 | 33 | 83% |
| Phase 5: Web Dashboard | 40 | 40 | 100% |
| Phase 6: Docker & Deployment | 28 | 0 | 0% |
| **Total** | **221** | **179** | **81%** |
| **Total** | **221** | **186** | **84%** |
---

304
tests/test_web/conftest.py Normal file
View File

@@ -0,0 +1,304 @@
"""Web dashboard test fixtures."""
import json
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from httpx import Response
from meshcore_hub.web.app import create_app
class MockHttpClient:
"""Mock HTTP client for testing web routes."""
def __init__(self) -> None:
"""Initialize mock client with default responses."""
self._responses: dict[str, dict[str, Any]] = {}
self._default_responses()
def _default_responses(self) -> None:
"""Set up default mock API responses."""
# Default stats response
self._responses["GET:/api/v1/dashboard/stats"] = {
"status_code": 200,
"json": {
"total_nodes": 10,
"active_nodes": 5,
"total_messages": 100,
"messages_today": 15,
"total_advertisements": 50,
"channel_message_counts": {"0": 30, "1": 20},
},
}
# Default nodes list response
self._responses["GET:/api/v1/nodes"] = {
"status_code": 200,
"json": {
"items": [
{
"id": "node-1",
"public_key": "abc123def456abc123def456abc123de",
"name": "Node One",
"adv_type": "REPEATER",
"last_seen": "2024-01-01T12:00:00Z",
"tags": [],
},
{
"id": "node-2",
"public_key": "def456abc123def456abc123def456ab",
"name": "Node Two",
"adv_type": "CLIENT",
"last_seen": "2024-01-01T11:00:00Z",
"tags": [
{"key": "lat", "value": "40.7128"},
{"key": "lon", "value": "-74.0060"},
],
},
],
"total": 2,
},
}
# Default single node response
self._responses["GET:/api/v1/nodes/abc123def456abc123def456abc123de"] = {
"status_code": 200,
"json": {
"id": "node-1",
"public_key": "abc123def456abc123def456abc123de",
"name": "Node One",
"adv_type": "REPEATER",
"last_seen": "2024-01-01T12:00:00Z",
"tags": [],
},
}
# Default messages response
self._responses["GET:/api/v1/messages"] = {
"status_code": 200,
"json": {
"items": [
{
"id": "msg-1",
"message_type": "direct",
"pubkey_prefix": "abc123",
"text": "Hello World",
"received_at": "2024-01-01T12:00:00Z",
"snr": -5.5,
"hops": 2,
},
{
"id": "msg-2",
"message_type": "channel",
"channel_idx": 0,
"text": "Channel message",
"received_at": "2024-01-01T11:00:00Z",
"snr": None,
"hops": None,
},
],
"total": 2,
},
}
# Default advertisements response
self._responses["GET:/api/v1/advertisements"] = {
"status_code": 200,
"json": {
"items": [
{
"id": "adv-1",
"public_key": "abc123def456abc123def456abc123de",
"name": "Node One",
"adv_type": "REPEATER",
"received_at": "2024-01-01T12:00:00Z",
},
],
"total": 1,
},
}
# Default telemetry response
self._responses["GET:/api/v1/telemetry"] = {
"status_code": 200,
"json": {
"items": [
{
"id": "tel-1",
"node_public_key": "abc123def456abc123def456abc123de",
"parsed_data": {"battery_level": 85.5},
"received_at": "2024-01-01T12:00:00Z",
},
],
"total": 1,
},
}
# Health check response
self._responses["GET:/health"] = {
"status_code": 200,
"json": {"status": "healthy"},
}
def set_response(
self, method: str, path: str, status_code: int = 200, json_data: Any = None
) -> None:
"""Set a custom response for a specific endpoint.
Args:
method: HTTP method (GET, POST, etc.)
path: URL path
status_code: Response status code
json_data: JSON response body
"""
key = f"{method}:{path}"
self._responses[key] = {
"status_code": status_code,
"json": json_data,
}
def _create_response(self, key: str) -> Response:
"""Create a mock response for a given key."""
response_data = self._responses.get(key)
if response_data is None:
# Return 404 for unknown endpoints
response = MagicMock(spec=Response)
response.status_code = 404
response.json.return_value = {"detail": "Not found"}
return response
response = MagicMock(spec=Response)
response.status_code = response_data["status_code"]
response.json.return_value = response_data["json"]
return response
async def get(self, path: str, params: dict | None = None) -> Response:
"""Mock GET request."""
# Try exact match first
key = f"GET:{path}"
if key in self._responses:
return self._create_response(key)
# Try without query params for list endpoints
base_path = path.split("?")[0]
key = f"GET:{base_path}"
return self._create_response(key)
async def post(
self, path: str, json: dict | None = None, params: dict | None = None
) -> Response:
"""Mock POST request."""
key = f"POST:{path}"
return self._create_response(key)
async def aclose(self) -> None:
"""Mock close method."""
pass
@pytest.fixture
def mock_http_client() -> MockHttpClient:
"""Create a mock HTTP client."""
return MockHttpClient()
@pytest.fixture
def web_app(mock_http_client: MockHttpClient) -> Any:
"""Create a web app with mocked HTTP client."""
app = create_app(
api_url="http://localhost:8000",
api_key="test-api-key",
network_name="Test Network",
network_city="Test City",
network_country="Test Country",
network_location=(40.7128, -74.0060),
network_radio_config="Test Radio Config",
network_contact_email="test@example.com",
network_contact_discord="https://discord.gg/test",
members_file=None,
)
# Override the lifespan to use our mock client
app.state.http_client = mock_http_client
return app
@pytest.fixture
def client(web_app: Any, mock_http_client: MockHttpClient) -> TestClient:
"""Create a test client for the web app.
Note: We don't use the context manager to skip lifespan events
since we've already set up the mock client.
"""
# Ensure the mock client is attached
web_app.state.http_client = mock_http_client
return TestClient(web_app, raise_server_exceptions=True)
@pytest.fixture
def members_file() -> Any:
"""Create a temporary members JSON file."""
members_data = {
"members": [
{
"name": "Alice",
"callsign": "W1ABC",
"role": "Admin",
"contact": "alice@example.com",
},
{
"name": "Bob",
"callsign": "W2XYZ",
"role": "Member",
"contact": None,
},
]
}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as f:
json.dump(members_data, f)
f.flush()
yield f.name
# Cleanup
Path(f.name).unlink(missing_ok=True)
@pytest.fixture
def web_app_with_members(
mock_http_client: MockHttpClient, members_file: str
) -> Any:
"""Create a web app with a members file configured."""
app = create_app(
api_url="http://localhost:8000",
api_key="test-api-key",
network_name="Test Network",
network_city="Test City",
network_country="Test Country",
network_location=(40.7128, -74.0060),
network_radio_config="Test Radio Config",
network_contact_email="test@example.com",
network_contact_discord="https://discord.gg/test",
members_file=members_file,
)
app.state.http_client = mock_http_client
return app
@pytest.fixture
def client_with_members(
web_app_with_members: Any, mock_http_client: MockHttpClient
) -> TestClient:
"""Create a test client with members file configured."""
web_app_with_members.state.http_client = mock_http_client
return TestClient(web_app_with_members, raise_server_exceptions=True)

View File

@@ -0,0 +1,56 @@
"""Tests for the home page route."""
import pytest
from fastapi.testclient import TestClient
class TestHomePage:
"""Tests for the home page."""
def test_home_returns_200(self, client: TestClient) -> None:
"""Test that home page returns 200 status code."""
response = client.get("/")
assert response.status_code == 200
def test_home_returns_html(self, client: TestClient) -> None:
"""Test that home page returns HTML content."""
response = client.get("/")
assert "text/html" in response.headers["content-type"]
def test_home_contains_network_name(self, client: TestClient) -> None:
"""Test that home page contains the network name."""
response = client.get("/")
assert "Test Network" in response.text
def test_home_contains_network_city(self, client: TestClient) -> None:
"""Test that home page contains the network city."""
response = client.get("/")
assert "Test City" in response.text
def test_home_contains_network_country(self, client: TestClient) -> None:
"""Test that home page contains the network country."""
response = client.get("/")
assert "Test Country" in response.text
def test_home_contains_radio_config(self, client: TestClient) -> None:
"""Test that home page contains the radio configuration."""
response = client.get("/")
assert "Test Radio Config" in response.text
def test_home_contains_contact_email(self, client: TestClient) -> None:
"""Test that home page contains the contact email."""
response = client.get("/")
assert "test@example.com" in response.text
def test_home_contains_discord_link(self, client: TestClient) -> None:
"""Test that home page contains the Discord link."""
response = client.get("/")
assert "discord.gg/test" in response.text
def test_home_contains_navigation(self, client: TestClient) -> None:
"""Test that home page contains navigation links."""
response = client.get("/")
# Check for navigation links to other pages
assert 'href="/"' in response.text or 'href=""' in response.text
assert 'href="/nodes"' in response.text or "/nodes" in response.text
assert 'href="/messages"' in response.text or "/messages" in response.text

174
tests/test_web/test_map.py Normal file
View File

@@ -0,0 +1,174 @@
"""Tests for the map page routes."""
import pytest
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

View File

@@ -0,0 +1,140 @@
"""Tests for the members page route."""
import json
import tempfile
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from meshcore_hub.web.routes.members import load_members
class TestMembersPage:
"""Tests for the members page."""
def test_members_returns_200(self, client: TestClient) -> None:
"""Test that members page returns 200 status code."""
response = client.get("/members")
assert response.status_code == 200
def test_members_returns_html(self, client: TestClient) -> None:
"""Test that members page returns HTML content."""
response = client.get("/members")
assert "text/html" in response.headers["content-type"]
def test_members_contains_network_name(self, client: TestClient) -> None:
"""Test that members page contains the network name."""
response = client.get("/members")
assert "Test Network" in response.text
def test_members_without_file_shows_empty(self, client: TestClient) -> None:
"""Test that members page with no file shows no members."""
response = client.get("/members")
# Should still render successfully
assert response.status_code == 200
def test_members_with_file_shows_members(
self, client_with_members: TestClient
) -> None:
"""Test that members page with file shows member data."""
response = client_with_members.get("/members")
assert response.status_code == 200
# Check for member data
assert "Alice" in response.text
assert "Bob" in response.text
assert "W1ABC" in response.text
assert "W2XYZ" in response.text
assert "Admin" in response.text
class TestLoadMembers:
"""Tests for the load_members function."""
def test_load_members_none_path(self) -> None:
"""Test load_members with None path returns empty list."""
result = load_members(None)
assert result == []
def test_load_members_nonexistent_file(self) -> None:
"""Test load_members with nonexistent file returns empty list."""
result = load_members("/nonexistent/path/members.json")
assert result == []
def test_load_members_list_format(self) -> None:
"""Test load_members with list format JSON."""
members_data = [
{"name": "Alice", "callsign": "W1ABC"},
{"name": "Bob", "callsign": "W2XYZ"},
]
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as f:
json.dump(members_data, f)
f.flush()
path = f.name
try:
result = load_members(path)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "Bob"
finally:
Path(path).unlink(missing_ok=True)
def test_load_members_dict_format(self) -> None:
"""Test load_members with dict format JSON (members key)."""
members_data = {
"members": [
{"name": "Alice", "callsign": "W1ABC"},
{"name": "Bob", "callsign": "W2XYZ"},
]
}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as f:
json.dump(members_data, f)
f.flush()
path = f.name
try:
result = load_members(path)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "Bob"
finally:
Path(path).unlink(missing_ok=True)
def test_load_members_invalid_json(self) -> None:
"""Test load_members with invalid JSON returns empty list."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as f:
f.write("not valid json {")
f.flush()
path = f.name
try:
result = load_members(path)
assert result == []
finally:
Path(path).unlink(missing_ok=True)
def test_load_members_dict_without_members_key(self) -> None:
"""Test load_members with dict but no members key returns empty list."""
data = {"other_key": "value"}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as f:
json.dump(data, f)
f.flush()
path = f.name
try:
result = load_members(path)
assert result == []
finally:
Path(path).unlink(missing_ok=True)

View File

@@ -0,0 +1,117 @@
"""Tests for the messages page route."""
import pytest
from fastapi.testclient import TestClient
from tests.test_web.conftest import MockHttpClient
class TestMessagesPage:
"""Tests for the messages page."""
def test_messages_returns_200(self, client: TestClient) -> None:
"""Test that messages page returns 200 status code."""
response = client.get("/messages")
assert response.status_code == 200
def test_messages_returns_html(self, client: TestClient) -> None:
"""Test that messages page returns HTML content."""
response = client.get("/messages")
assert "text/html" in response.headers["content-type"]
def test_messages_contains_network_name(self, client: TestClient) -> None:
"""Test that messages page contains the network name."""
response = client.get("/messages")
assert "Test Network" in response.text
def test_messages_displays_message_list(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that messages page displays messages from API."""
response = client.get("/messages")
assert response.status_code == 200
# Check for message data from mock
assert "Hello World" in response.text
assert "Channel message" in response.text
def test_messages_displays_message_types(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that messages page displays message types."""
response = client.get("/messages")
# Should show message types
assert "direct" in response.text.lower() or "contact" in response.text.lower()
assert "channel" in response.text.lower()
class TestMessagesPageFilters:
"""Tests for messages page filtering."""
def test_messages_with_type_filter(self, client: TestClient) -> None:
"""Test messages page with message type filter."""
response = client.get("/messages?message_type=direct")
assert response.status_code == 200
def test_messages_with_channel_filter(self, client: TestClient) -> None:
"""Test messages page with channel filter."""
response = client.get("/messages?channel_idx=0")
assert response.status_code == 200
def test_messages_with_search(self, client: TestClient) -> None:
"""Test messages page with search parameter."""
response = client.get("/messages?search=hello")
assert response.status_code == 200
def test_messages_with_pagination(self, client: TestClient) -> None:
"""Test messages page with pagination parameters."""
response = client.get("/messages?page=1&limit=25")
assert response.status_code == 200
def test_messages_page_2(self, client: TestClient) -> None:
"""Test messages page 2."""
response = client.get("/messages?page=2")
assert response.status_code == 200
def test_messages_with_all_filters(self, client: TestClient) -> None:
"""Test messages page with multiple filters."""
response = client.get(
"/messages?message_type=channel&channel_idx=1&page=1&limit=10"
)
assert response.status_code == 200
class TestMessagesPageAPIErrors:
"""Tests for messages page handling API errors."""
def test_messages_handles_api_error(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that messages page handles API errors gracefully."""
mock_http_client.set_response(
"GET", "/api/v1/messages", 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("/messages")
# Should still return 200 (page renders with empty list)
assert response.status_code == 200
def test_messages_handles_api_not_found(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that messages page handles API 404 gracefully."""
mock_http_client.set_response(
"GET",
"/api/v1/messages",
status_code=404,
json_data={"detail": "Not found"},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/messages")
# Should still return 200 (page renders with empty list)
assert response.status_code == 200

View File

@@ -0,0 +1,85 @@
"""Tests for the network overview page route."""
import pytest
from fastapi.testclient import TestClient
from tests.test_web.conftest import MockHttpClient
class TestNetworkPage:
"""Tests for the network overview page."""
def test_network_returns_200(self, client: TestClient) -> None:
"""Test that network page returns 200 status code."""
response = client.get("/network")
assert response.status_code == 200
def test_network_returns_html(self, client: TestClient) -> None:
"""Test that network page returns HTML content."""
response = client.get("/network")
assert "text/html" in response.headers["content-type"]
def test_network_contains_network_name(self, client: TestClient) -> None:
"""Test that network page contains the network name."""
response = client.get("/network")
assert "Test Network" in response.text
def test_network_displays_stats(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that network page displays statistics."""
response = client.get("/network")
# Check for stats from mock response
assert response.status_code == 200
# The mock returns total_nodes: 10, active_nodes: 5, etc.
# These should be displayed in the page
assert "10" in response.text # total_nodes
assert "5" in response.text # active_nodes
def test_network_displays_message_counts(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that network page displays message counts."""
response = client.get("/network")
assert response.status_code == 200
# Mock returns total_messages: 100, messages_today: 15
assert "100" in response.text
assert "15" in response.text
class TestNetworkPageAPIErrors:
"""Tests for network page handling API errors."""
def test_network_handles_api_error(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that network page handles API errors gracefully."""
# Set error response for stats endpoint
mock_http_client.set_response(
"GET", "/api/v1/dashboard/stats", 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("/network")
# Should still return 200 (page renders with defaults)
assert response.status_code == 200
def test_network_handles_api_not_found(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that network page handles API 404 gracefully."""
mock_http_client.set_response(
"GET",
"/api/v1/dashboard/stats",
status_code=404,
json_data={"detail": "Not found"},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/network")
# Should still return 200 (page renders with defaults)
assert response.status_code == 200

View File

@@ -0,0 +1,137 @@
"""Tests for the nodes page routes."""
import pytest
from fastapi.testclient import TestClient
from tests.test_web.conftest import MockHttpClient
class TestNodesListPage:
"""Tests for the nodes list page."""
def test_nodes_returns_200(self, client: TestClient) -> None:
"""Test that nodes page returns 200 status code."""
response = client.get("/nodes")
assert response.status_code == 200
def test_nodes_returns_html(self, client: TestClient) -> None:
"""Test that nodes page returns HTML content."""
response = client.get("/nodes")
assert "text/html" in response.headers["content-type"]
def test_nodes_contains_network_name(self, client: TestClient) -> None:
"""Test that nodes page contains the network name."""
response = client.get("/nodes")
assert "Test Network" in response.text
def test_nodes_displays_node_list(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that nodes page displays node data from API."""
response = client.get("/nodes")
assert response.status_code == 200
# Check for node data from mock
assert "Node One" in response.text
assert "Node Two" in response.text
assert "REPEATER" in response.text
assert "CLIENT" in response.text
def test_nodes_displays_public_keys(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that nodes page displays public keys."""
response = client.get("/nodes")
# Should show truncated or full public keys
assert "abc123" in response.text or "def456" in response.text
def test_nodes_with_search_param(self, client: TestClient) -> None:
"""Test nodes page with search parameter."""
response = client.get("/nodes?search=test")
assert response.status_code == 200
def test_nodes_with_adv_type_filter(self, client: TestClient) -> None:
"""Test nodes page with adv_type filter."""
response = client.get("/nodes?adv_type=REPEATER")
assert response.status_code == 200
def test_nodes_with_pagination(self, client: TestClient) -> None:
"""Test nodes page with pagination parameters."""
response = client.get("/nodes?page=1&limit=10")
assert response.status_code == 200
def test_nodes_page_2(self, client: TestClient) -> None:
"""Test nodes page 2."""
response = client.get("/nodes?page=2")
assert response.status_code == 200
class TestNodeDetailPage:
"""Tests for the node detail page."""
def test_node_detail_returns_200(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that node detail page returns 200 status code."""
response = client.get("/nodes/abc123def456abc123def456abc123de")
assert response.status_code == 200
def test_node_detail_returns_html(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that node detail page returns HTML content."""
response = client.get("/nodes/abc123def456abc123def456abc123de")
assert "text/html" in response.headers["content-type"]
def test_node_detail_displays_node_info(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that node detail page displays node information."""
response = client.get("/nodes/abc123def456abc123def456abc123de")
assert response.status_code == 200
# Should display node details
assert "Node One" in response.text
assert "REPEATER" in response.text
def test_node_detail_displays_public_key(
self, client: TestClient, mock_http_client: MockHttpClient
) -> None:
"""Test that node detail page displays the full public key."""
response = client.get("/nodes/abc123def456abc123def456abc123de")
assert "abc123def456abc123def456abc123de" in response.text
class TestNodesPageAPIErrors:
"""Tests for nodes pages handling API errors."""
def test_nodes_handles_api_error(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that nodes page 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("/nodes")
# Should still return 200 (page renders with empty list)
assert response.status_code == 200
def test_node_detail_handles_not_found(
self, web_app: any, mock_http_client: MockHttpClient
) -> None:
"""Test that node detail page handles 404 from API."""
mock_http_client.set_response(
"GET",
"/api/v1/nodes/nonexistent",
status_code=404,
json_data={"detail": "Node not found"},
)
web_app.state.http_client = mock_http_client
client = TestClient(web_app, raise_server_exceptions=True)
response = client.get("/nodes/nonexistent")
# Should still return 200 (page renders but shows no node)
assert response.status_code == 200