Phase 4: Implement REST API component

- Add FastAPI application with lifespan management
- Implement bearer token authentication (read/admin levels)
- Create comprehensive REST API routes:
  - Nodes: list, get by public key
  - Node tags: CRUD operations
  - Messages: list with filters, get by ID
  - Advertisements: list with filters, get by ID
  - Telemetry: list with filters, get by ID
  - Trace paths: list with filters, get by ID
  - Commands: send message, channel message, advertisement
  - Dashboard: stats API and HTML dashboard
- Add API CLI command for running the server
- Create API test suite with 44 passing tests

Routes use proper RESTful status codes (201 Created, 204 No Content).
Authentication is optional - when keys not configured, endpoints are open.
This commit is contained in:
Claude
2025-12-02 23:41:32 +00:00
parent 2617dace7b
commit aefa9b735f
24 changed files with 2304 additions and 63 deletions
+262
View File
@@ -0,0 +1,262 @@
"""API test fixtures."""
import os
import tempfile
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from meshcore_hub.api.app import create_app
from meshcore_hub.api.dependencies import get_db_session, get_mqtt_client, get_db_manager
from meshcore_hub.common.database import DatabaseManager
from meshcore_hub.common.models import (
Advertisement,
Base,
Message,
Node,
NodeTag,
Telemetry,
TracePath,
)
@pytest.fixture
def test_db_path():
"""Create a temporary database file path."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
yield path
# Cleanup
if os.path.exists(path):
os.unlink(path)
@pytest.fixture
def api_db_engine(test_db_path):
"""Create a SQLite database engine for API testing."""
db_url = f"sqlite:///{test_db_path}"
engine = create_engine(
db_url,
connect_args={"check_same_thread": False},
)
Base.metadata.create_all(engine)
yield engine
Base.metadata.drop_all(engine)
engine.dispose()
@pytest.fixture
def api_db_session(api_db_engine):
"""Create a database session for API testing."""
Session = sessionmaker(bind=api_db_engine)
session = Session()
yield session
session.close()
@pytest.fixture
def mock_mqtt():
"""Create a mock MQTT client."""
mock = MagicMock()
mock.connect.return_value = None
mock.start_background.return_value = None
mock.stop.return_value = None
mock.disconnect.return_value = None
mock.publish_command.return_value = None
return mock
@pytest.fixture
def mock_db_manager(api_db_engine):
"""Create a mock database manager using the test engine."""
manager = MagicMock(spec=DatabaseManager)
Session = sessionmaker(bind=api_db_engine)
manager.get_session = lambda: Session()
return manager
@pytest.fixture
def app_no_auth(test_db_path, api_db_engine, mock_mqtt, mock_db_manager):
"""Create a FastAPI app with no authentication required."""
db_url = f"sqlite:///{test_db_path}"
# Patch the global db_manager to avoid lifespan issues
with patch("meshcore_hub.api.app._db_manager", mock_db_manager):
app = create_app(
database_url=db_url,
read_key=None,
admin_key=None,
)
# Create session maker for this test engine
Session = sessionmaker(bind=api_db_engine)
def override_get_db_manager(request=None):
return mock_db_manager
def override_get_db_session():
session = Session()
try:
yield session
finally:
session.close()
def override_get_mqtt_client(request=None):
return mock_mqtt
app.dependency_overrides[get_db_manager] = override_get_db_manager
app.dependency_overrides[get_db_session] = override_get_db_session
app.dependency_overrides[get_mqtt_client] = override_get_mqtt_client
yield app
@pytest.fixture
def app_with_auth(test_db_path, api_db_engine, mock_mqtt, mock_db_manager):
"""Create a FastAPI app with authentication enabled."""
db_url = f"sqlite:///{test_db_path}"
with patch("meshcore_hub.api.app._db_manager", mock_db_manager):
app = create_app(
database_url=db_url,
read_key="test-read-key",
admin_key="test-admin-key",
)
Session = sessionmaker(bind=api_db_engine)
def override_get_db_manager(request=None):
return mock_db_manager
def override_get_db_session():
session = Session()
try:
yield session
finally:
session.close()
def override_get_mqtt_client(request=None):
return mock_mqtt
app.dependency_overrides[get_db_manager] = override_get_db_manager
app.dependency_overrides[get_db_session] = override_get_db_session
app.dependency_overrides[get_mqtt_client] = override_get_mqtt_client
yield app
@pytest.fixture
def client_no_auth(app_no_auth, mock_db_manager):
"""Create a test client with no authentication.
Uses raise_server_exceptions=False to skip lifespan events.
"""
# Don't use context manager to skip lifespan
client = TestClient(app_no_auth, raise_server_exceptions=True)
yield client
@pytest.fixture
def client_with_auth(app_with_auth, mock_db_manager):
"""Create a test client with authentication enabled.
Uses raise_server_exceptions=False to skip lifespan events.
"""
client = TestClient(app_with_auth, raise_server_exceptions=True)
yield client
@pytest.fixture
def sample_node(api_db_session):
"""Create a sample node in the database."""
node = Node(
public_key="abc123def456abc123def456abc123de",
name="Test Node",
adv_type="REPEATER",
first_seen=datetime.now(timezone.utc),
last_seen=datetime.now(timezone.utc),
)
api_db_session.add(node)
api_db_session.commit()
api_db_session.refresh(node)
return node
@pytest.fixture
def sample_node_tag(api_db_session, sample_node):
"""Create a sample node tag in the database."""
tag = NodeTag(
node_id=sample_node.id,
key="environment",
value="production",
)
api_db_session.add(tag)
api_db_session.commit()
api_db_session.refresh(tag)
return tag
@pytest.fixture
def sample_message(api_db_session):
"""Create a sample message in the database."""
message = Message(
message_type="direct",
pubkey_prefix="abc123",
text="Hello World",
received_at=datetime.now(timezone.utc),
)
api_db_session.add(message)
api_db_session.commit()
api_db_session.refresh(message)
return message
@pytest.fixture
def sample_advertisement(api_db_session):
"""Create a sample advertisement in the database."""
advert = Advertisement(
public_key="abc123def456abc123def456abc123de",
name="TestNode",
adv_type="REPEATER",
received_at=datetime.now(timezone.utc),
)
api_db_session.add(advert)
api_db_session.commit()
api_db_session.refresh(advert)
return advert
@pytest.fixture
def sample_telemetry(api_db_session):
"""Create a sample telemetry record in the database."""
telemetry = Telemetry(
node_public_key="abc123def456abc123def456abc123de",
parsed_data={
"battery_level": 85.5,
"temperature": 25.3,
},
received_at=datetime.now(timezone.utc),
)
api_db_session.add(telemetry)
api_db_session.commit()
api_db_session.refresh(telemetry)
return telemetry
@pytest.fixture
def sample_trace_path(api_db_session):
"""Create a sample trace path in the database."""
trace = TracePath(
initiator_tag=12345,
path_hashes=["abc123", "def456", "ghi789"],
hop_count=3,
received_at=datetime.now(timezone.utc),
)
api_db_session.add(trace)
api_db_session.commit()
api_db_session.refresh(trace)
return trace
+62
View File
@@ -0,0 +1,62 @@
"""Tests for advertisement API routes."""
import pytest
class TestListAdvertisements:
"""Tests for GET /advertisements endpoint."""
def test_list_advertisements_empty(self, client_no_auth):
"""Test listing advertisements when database is empty."""
response = client_no_auth.get("/api/v1/advertisements")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_advertisements_with_data(self, client_no_auth, sample_advertisement):
"""Test listing advertisements with data in database."""
response = client_no_auth.get("/api/v1/advertisements")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["total"] == 1
assert data["items"][0]["public_key"] == sample_advertisement.public_key
assert data["items"][0]["adv_type"] == sample_advertisement.adv_type
def test_list_advertisements_filter_by_public_key(
self, client_no_auth, sample_advertisement
):
"""Test filtering advertisements by public key."""
response = client_no_auth.get(
f"/api/v1/advertisements?public_key={sample_advertisement.public_key}"
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
response = client_no_auth.get(
"/api/v1/advertisements?public_key=nonexistent"
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 0
class TestGetAdvertisement:
"""Tests for GET /advertisements/{id} endpoint."""
def test_get_advertisement_success(self, client_no_auth, sample_advertisement):
"""Test getting a specific advertisement."""
response = client_no_auth.get(
f"/api/v1/advertisements/{sample_advertisement.id}"
)
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_advertisement.id
assert data["public_key"] == sample_advertisement.public_key
def test_get_advertisement_not_found(self, client_no_auth):
"""Test getting a non-existent advertisement."""
response = client_no_auth.get("/api/v1/advertisements/nonexistent-id")
assert response.status_code == 404
+106
View File
@@ -0,0 +1,106 @@
"""Tests for API authentication."""
import pytest
class TestAuthenticationFlow:
"""Tests for authentication behavior."""
def test_no_auth_when_keys_not_configured(self, client_no_auth):
"""Test that no auth is required when keys are not configured."""
# All endpoints should work without auth
response = client_no_auth.get("/api/v1/nodes")
assert response.status_code == 200
response = client_no_auth.get("/api/v1/messages")
assert response.status_code == 200
response = client_no_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Test",
},
)
assert response.status_code == 200
def test_read_endpoints_accept_read_key(self, client_with_auth):
"""Test that read endpoints accept read key."""
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": "Bearer test-read-key"},
)
assert response.status_code == 200
def test_read_endpoints_accept_admin_key(self, client_with_auth):
"""Test that read endpoints accept admin key."""
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": "Bearer test-admin-key"},
)
assert response.status_code == 200
def test_admin_endpoints_reject_read_key(self, client_with_auth):
"""Test that admin endpoints reject read key."""
response = client_with_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Test",
},
headers={"Authorization": "Bearer test-read-key"},
)
assert response.status_code == 403
def test_admin_endpoints_accept_admin_key(self, client_with_auth):
"""Test that admin endpoints accept admin key."""
response = client_with_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Test",
},
headers={"Authorization": "Bearer test-admin-key"},
)
assert response.status_code == 200
def test_invalid_key_rejected(self, client_with_auth):
"""Test that invalid keys are rejected."""
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": "Bearer invalid-key"},
)
assert response.status_code == 401
def test_missing_bearer_prefix_rejected(self, client_with_auth):
"""Test that tokens without Bearer prefix are rejected."""
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": "test-read-key"},
)
assert response.status_code == 401
def test_empty_auth_header_rejected(self, client_with_auth):
"""Test that empty auth headers are rejected."""
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": ""},
)
assert response.status_code == 401
class TestHealthEndpoint:
"""Tests for health check endpoint."""
def test_health_no_auth(self, client_no_auth):
"""Test health endpoint without auth."""
response = client_no_auth.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
def test_health_with_auth_configured(self, client_with_auth):
"""Test health endpoint works even when auth is configured."""
# Health endpoint should always be accessible
response = client_with_auth.get("/health")
assert response.status_code == 200
+118
View File
@@ -0,0 +1,118 @@
"""Tests for command API routes."""
import pytest
class TestSendMessage:
"""Tests for POST /commands/send-message endpoint."""
def test_send_message_success(self, client_no_auth, mock_mqtt):
"""Test sending a direct message."""
response = client_no_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Hello World",
},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "queued" in data["message"].lower()
def test_send_message_requires_admin(self, client_with_auth):
"""Test sending message requires admin authentication."""
# Without auth
response = client_with_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Hello",
},
)
assert response.status_code == 401
# With read key (not admin)
response = client_with_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Hello",
},
headers={"Authorization": "Bearer test-read-key"},
)
assert response.status_code == 403
# With admin key
response = client_with_auth.post(
"/api/v1/commands/send-message",
json={
"destination": "abc123def456abc123def456abc123de",
"text": "Hello",
},
headers={"Authorization": "Bearer test-admin-key"},
)
assert response.status_code == 200
class TestSendChannelMessage:
"""Tests for POST /commands/send-channel-message endpoint."""
def test_send_channel_message_success(self, client_no_auth, mock_mqtt):
"""Test sending a channel message."""
response = client_no_auth.post(
"/api/v1/commands/send-channel-message",
json={
"channel_idx": 1,
"text": "Hello Channel",
},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "channel 1" in data["message"].lower()
def test_send_channel_message_requires_admin(self, client_with_auth):
"""Test sending channel message requires admin authentication."""
response = client_with_auth.post(
"/api/v1/commands/send-channel-message",
json={
"channel_idx": 1,
"text": "Hello",
},
)
assert response.status_code == 401
class TestSendAdvertisement:
"""Tests for POST /commands/send-advertisement endpoint."""
def test_send_advertisement_success(self, client_no_auth, mock_mqtt):
"""Test sending an advertisement."""
response = client_no_auth.post(
"/api/v1/commands/send-advertisement",
json={"flood": False},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "advertisement" in data["message"].lower()
def test_send_advertisement_with_flood(self, client_no_auth, mock_mqtt):
"""Test sending an advertisement with flood enabled."""
response = client_no_auth.post(
"/api/v1/commands/send-advertisement",
json={"flood": True},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "flood=True" in data["message"]
def test_send_advertisement_requires_admin(self, client_with_auth):
"""Test sending advertisement requires admin authentication."""
response = client_with_auth.post(
"/api/v1/commands/send-advertisement",
json={"flood": False},
)
assert response.status_code == 401
+62
View File
@@ -0,0 +1,62 @@
"""Tests for dashboard API routes."""
import pytest
class TestDashboardStats:
"""Tests for GET /dashboard/stats endpoint."""
def test_get_stats_empty(self, client_no_auth):
"""Test getting stats with empty database."""
response = client_no_auth.get("/api/v1/dashboard/stats")
assert response.status_code == 200
data = response.json()
assert data["total_nodes"] == 0
assert data["active_nodes"] == 0
assert data["total_messages"] == 0
assert data["messages_today"] == 0
assert data["total_advertisements"] == 0
assert data["channel_message_counts"] == {}
def test_get_stats_with_data(
self, client_no_auth, sample_node, sample_message, sample_advertisement
):
"""Test getting stats with data in database."""
response = client_no_auth.get("/api/v1/dashboard/stats")
assert response.status_code == 200
data = response.json()
assert data["total_nodes"] == 1
assert data["active_nodes"] == 1 # Node was just created
assert data["total_messages"] == 1
assert data["total_advertisements"] == 1
class TestDashboardHtml:
"""Tests for GET /dashboard/dashboard endpoint."""
def test_dashboard_html_response(self, client_no_auth):
"""Test dashboard returns HTML."""
response = client_no_auth.get("/api/v1/dashboard/dashboard")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "<!DOCTYPE html>" in response.text
assert "MeshCore Hub Dashboard" in response.text
def test_dashboard_contains_stats(
self, client_no_auth, sample_node, sample_message
):
"""Test dashboard HTML contains stat values."""
response = client_no_auth.get("/api/v1/dashboard/dashboard")
assert response.status_code == 200
# Check that stats are present
assert "Total Nodes" in response.text
assert "Active Nodes" in response.text
assert "Total Messages" in response.text
def test_dashboard_contains_recent_data(self, client_no_auth, sample_node):
"""Test dashboard HTML contains recent nodes."""
response = client_no_auth.get("/api/v1/dashboard/dashboard")
assert response.status_code == 200
assert "Recent Nodes" in response.text
# The node name should appear in the table
assert sample_node.name in response.text
+62
View File
@@ -0,0 +1,62 @@
"""Tests for message API routes."""
import pytest
class TestListMessages:
"""Tests for GET /messages endpoint."""
def test_list_messages_empty(self, client_no_auth):
"""Test listing messages when database is empty."""
response = client_no_auth.get("/api/v1/messages")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_messages_with_data(self, client_no_auth, sample_message):
"""Test listing messages with data in database."""
response = client_no_auth.get("/api/v1/messages")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["total"] == 1
assert data["items"][0]["text"] == sample_message.text
assert data["items"][0]["message_type"] == sample_message.message_type
def test_list_messages_filter_by_type(self, client_no_auth, sample_message):
"""Test filtering messages by type."""
response = client_no_auth.get("/api/v1/messages?message_type=direct")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
response = client_no_auth.get("/api/v1/messages?message_type=channel")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 0
def test_list_messages_pagination(self, client_no_auth):
"""Test message list pagination parameters."""
response = client_no_auth.get("/api/v1/messages?limit=25&offset=10")
assert response.status_code == 200
data = response.json()
assert data["limit"] == 25
assert data["offset"] == 10
class TestGetMessage:
"""Tests for GET /messages/{id} endpoint."""
def test_get_message_success(self, client_no_auth, sample_message):
"""Test getting a specific message."""
response = client_no_auth.get(f"/api/v1/messages/{sample_message.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_message.id
assert data["text"] == sample_message.text
def test_get_message_not_found(self, client_no_auth):
"""Test getting a non-existent message."""
response = client_no_auth.get("/api/v1/messages/nonexistent-id")
assert response.status_code == 404
+136
View File
@@ -0,0 +1,136 @@
"""Tests for node API routes."""
import pytest
class TestListNodes:
"""Tests for GET /nodes endpoint."""
def test_list_nodes_empty(self, client_no_auth):
"""Test listing nodes when database is empty."""
response = client_no_auth.get("/api/v1/nodes")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_nodes_with_data(self, client_no_auth, sample_node):
"""Test listing nodes with data in database."""
response = client_no_auth.get("/api/v1/nodes")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["total"] == 1
assert data["items"][0]["public_key"] == sample_node.public_key
assert data["items"][0]["name"] == sample_node.name
def test_list_nodes_pagination(self, client_no_auth, sample_node):
"""Test node list pagination parameters."""
response = client_no_auth.get("/api/v1/nodes?limit=10&offset=0")
assert response.status_code == 200
data = response.json()
assert data["limit"] == 10
assert data["offset"] == 0
def test_list_nodes_with_auth_required(self, client_with_auth):
"""Test listing nodes requires auth when configured."""
# Without auth header
response = client_with_auth.get("/api/v1/nodes")
assert response.status_code == 401
# With read key
response = client_with_auth.get(
"/api/v1/nodes",
headers={"Authorization": "Bearer test-read-key"},
)
assert response.status_code == 200
class TestGetNode:
"""Tests for GET /nodes/{public_key} endpoint."""
def test_get_node_success(self, client_no_auth, sample_node):
"""Test getting a specific node."""
response = client_no_auth.get(f"/api/v1/nodes/{sample_node.public_key}")
assert response.status_code == 200
data = response.json()
assert data["public_key"] == sample_node.public_key
assert data["name"] == sample_node.name
def test_get_node_not_found(self, client_no_auth):
"""Test getting a non-existent node."""
response = client_no_auth.get("/api/v1/nodes/nonexistent123")
assert response.status_code == 404
class TestNodeTags:
"""Tests for node tag endpoints."""
def test_create_node_tag(self, client_no_auth, sample_node):
"""Test creating a node tag."""
response = client_no_auth.post(
f"/api/v1/nodes/{sample_node.public_key}/tags",
json={"key": "location", "value": "building-a"},
)
assert response.status_code == 201 # Created
data = response.json()
assert data["key"] == "location"
assert data["value"] == "building-a"
def test_get_node_tag(self, client_no_auth, sample_node, sample_node_tag):
"""Test getting a specific node tag."""
response = client_no_auth.get(
f"/api/v1/nodes/{sample_node.public_key}/tags/{sample_node_tag.key}"
)
assert response.status_code == 200
data = response.json()
assert data["key"] == sample_node_tag.key
assert data["value"] == sample_node_tag.value
def test_update_node_tag(self, client_no_auth, sample_node, sample_node_tag):
"""Test updating a node tag."""
response = client_no_auth.put(
f"/api/v1/nodes/{sample_node.public_key}/tags/{sample_node_tag.key}",
json={"value": "staging"},
)
assert response.status_code == 200
data = response.json()
assert data["value"] == "staging"
def test_delete_node_tag(self, client_no_auth, sample_node, sample_node_tag):
"""Test deleting a node tag."""
response = client_no_auth.delete(
f"/api/v1/nodes/{sample_node.public_key}/tags/{sample_node_tag.key}"
)
assert response.status_code == 204 # No Content
# Verify it's deleted
response = client_no_auth.get(
f"/api/v1/nodes/{sample_node.public_key}/tags/{sample_node_tag.key}"
)
assert response.status_code == 404
def test_tag_crud_requires_admin(self, client_with_auth, sample_node):
"""Test that tag CRUD operations require admin auth."""
# Without auth
response = client_with_auth.post(
f"/api/v1/nodes/{sample_node.public_key}/tags",
json={"key": "test", "value": "test"},
)
assert response.status_code == 401
# With read key (not admin)
response = client_with_auth.post(
f"/api/v1/nodes/{sample_node.public_key}/tags",
json={"key": "test", "value": "test"},
headers={"Authorization": "Bearer test-read-key"},
)
assert response.status_code == 403
# With admin key
response = client_with_auth.post(
f"/api/v1/nodes/{sample_node.public_key}/tags",
json={"key": "test", "value": "test"},
headers={"Authorization": "Bearer test-admin-key"},
)
assert response.status_code == 201 # Created
+58
View File
@@ -0,0 +1,58 @@
"""Tests for telemetry API routes."""
import pytest
class TestListTelemetry:
"""Tests for GET /telemetry endpoint."""
def test_list_telemetry_empty(self, client_no_auth):
"""Test listing telemetry when database is empty."""
response = client_no_auth.get("/api/v1/telemetry")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_telemetry_with_data(self, client_no_auth, sample_telemetry):
"""Test listing telemetry with data in database."""
response = client_no_auth.get("/api/v1/telemetry")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["total"] == 1
assert data["items"][0]["node_public_key"] == sample_telemetry.node_public_key
assert data["items"][0]["parsed_data"] == sample_telemetry.parsed_data
def test_list_telemetry_filter_by_node(self, client_no_auth, sample_telemetry):
"""Test filtering telemetry by node public key."""
response = client_no_auth.get(
f"/api/v1/telemetry?node_public_key={sample_telemetry.node_public_key}"
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
response = client_no_auth.get(
"/api/v1/telemetry?node_public_key=nonexistent"
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 0
class TestGetTelemetry:
"""Tests for GET /telemetry/{id} endpoint."""
def test_get_telemetry_success(self, client_no_auth, sample_telemetry):
"""Test getting a specific telemetry record."""
response = client_no_auth.get(f"/api/v1/telemetry/{sample_telemetry.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_telemetry.id
assert data["node_public_key"] == sample_telemetry.node_public_key
def test_get_telemetry_not_found(self, client_no_auth):
"""Test getting a non-existent telemetry record."""
response = client_no_auth.get("/api/v1/telemetry/nonexistent-id")
assert response.status_code == 404
+42
View File
@@ -0,0 +1,42 @@
"""Tests for trace path API routes."""
import pytest
class TestListTracePaths:
"""Tests for GET /trace-paths endpoint."""
def test_list_trace_paths_empty(self, client_no_auth):
"""Test listing trace paths when database is empty."""
response = client_no_auth.get("/api/v1/trace-paths")
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_trace_paths_with_data(self, client_no_auth, sample_trace_path):
"""Test listing trace paths with data in database."""
response = client_no_auth.get("/api/v1/trace-paths")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["total"] == 1
assert data["items"][0]["path_hashes"] == sample_trace_path.path_hashes
assert data["items"][0]["hop_count"] == sample_trace_path.hop_count
class TestGetTracePath:
"""Tests for GET /trace-paths/{id} endpoint."""
def test_get_trace_path_success(self, client_no_auth, sample_trace_path):
"""Test getting a specific trace path."""
response = client_no_auth.get(f"/api/v1/trace-paths/{sample_trace_path.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == sample_trace_path.id
assert data["path_hashes"] == sample_trace_path.path_hashes
def test_get_trace_path_not_found(self, client_no_auth):
"""Test getting a non-existent trace path."""
response = client_no_auth.get("/api/v1/trace-paths/nonexistent-id")
assert response.status_code == 404