feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
227
backend/tests/integration/test_devices_api.py
Normal file
227
backend/tests/integration/test_devices_api.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Integration tests for the Device CRUD API endpoints.
|
||||
|
||||
Tests exercise /api/tenants/{tenant_id}/devices/* endpoints against
|
||||
real PostgreSQL+TimescaleDB with full auth + RLS enforcement.
|
||||
|
||||
All tests are independent and create their own test data.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _unique_suffix():
|
||||
"""Return a short unique suffix for test data."""
|
||||
return uuid.uuid4().hex[:8]
|
||||
|
||||
|
||||
class TestDevicesCRUD:
|
||||
"""Device list, create, get, update, delete endpoints."""
|
||||
|
||||
async def test_list_devices_empty(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
):
|
||||
"""GET /api/tenants/{tenant_id}/devices returns 200 with empty list."""
|
||||
auth = await auth_headers_factory(admin_session)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["items"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
async def test_create_device(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
):
|
||||
"""POST /api/tenants/{tenant_id}/devices creates a device and returns 201."""
|
||||
auth = await auth_headers_factory(admin_session, role="operator")
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
device_data = {
|
||||
"hostname": f"test-router-{uuid.uuid4().hex[:8]}",
|
||||
"ip_address": "192.168.88.1",
|
||||
"api_port": 8728,
|
||||
"api_ssl_port": 8729,
|
||||
"username": "admin",
|
||||
"password": "admin123",
|
||||
}
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/tenants/{tenant_id}/devices",
|
||||
json=device_data,
|
||||
headers=auth["headers"],
|
||||
)
|
||||
# create_device does TCP probe -- may fail in test env without real device
|
||||
# Accept either 201 (success) or 502/422 (connectivity check failure)
|
||||
if resp.status_code == 201:
|
||||
data = resp.json()
|
||||
assert data["hostname"] == device_data["hostname"]
|
||||
assert data["ip_address"] == device_data["ip_address"]
|
||||
assert "id" in data
|
||||
# Credentials should never be returned in response
|
||||
assert "password" not in data
|
||||
assert "username" not in data
|
||||
assert "encrypted_credentials" not in data
|
||||
|
||||
async def test_get_device(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET /api/tenants/{tenant_id}/devices/{device_id} returns correct device."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["id"] == str(device.id)
|
||||
assert data["hostname"] == device.hostname
|
||||
assert data["ip_address"] == device.ip_address
|
||||
|
||||
async def test_update_device(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""PUT /api/tenants/{tenant_id}/devices/{device_id} updates device fields."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id, role="operator"
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
device = await create_test_device(admin_session, tenant.id, hostname="old-hostname")
|
||||
await admin_session.commit()
|
||||
|
||||
update_data = {"hostname": f"new-hostname-{uuid.uuid4().hex[:8]}"}
|
||||
resp = await client.put(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}",
|
||||
json=update_data,
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["hostname"] == update_data["hostname"]
|
||||
|
||||
async def test_delete_device(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""DELETE /api/tenants/{tenant_id}/devices/{device_id} removes the device."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
# delete requires tenant_admin or above
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id, role="tenant_admin"
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
device = await create_test_device(admin_session, tenant.id)
|
||||
await admin_session.commit()
|
||||
|
||||
resp = await client.delete(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
|
||||
# Verify it's gone
|
||||
get_resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{device.id}",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert get_resp.status_code == 404
|
||||
|
||||
async def test_list_devices_with_status_filter(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
create_test_device,
|
||||
create_test_tenant,
|
||||
):
|
||||
"""GET /api/tenants/{tenant_id}/devices?status=online returns filtered results."""
|
||||
tenant = await create_test_tenant(admin_session)
|
||||
auth = await auth_headers_factory(
|
||||
admin_session, existing_tenant_id=tenant.id
|
||||
)
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
# Create devices with different statuses
|
||||
await create_test_device(
|
||||
admin_session, tenant.id, hostname="online-device", status="online"
|
||||
)
|
||||
await create_test_device(
|
||||
admin_session, tenant.id, hostname="offline-device", status="offline"
|
||||
)
|
||||
await admin_session.commit()
|
||||
|
||||
# Filter for online only
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices?status=online",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] >= 1
|
||||
for item in data["items"]:
|
||||
assert item["status"] == "online"
|
||||
|
||||
async def test_get_device_not_found(
|
||||
self,
|
||||
client,
|
||||
auth_headers_factory,
|
||||
admin_session,
|
||||
):
|
||||
"""GET /api/tenants/{tenant_id}/devices/{nonexistent} returns 404."""
|
||||
auth = await auth_headers_factory(admin_session)
|
||||
tenant_id = auth["tenant_id"]
|
||||
fake_id = str(uuid.uuid4())
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/tenants/{tenant_id}/devices/{fake_id}",
|
||||
headers=auth["headers"],
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_list_devices_unauthenticated(self, client):
|
||||
"""GET /api/tenants/{tenant_id}/devices without auth returns 401."""
|
||||
tenant_id = str(uuid.uuid4())
|
||||
resp = await client.get(f"/api/tenants/{tenant_id}/devices")
|
||||
assert resp.status_code == 401
|
||||
Reference in New Issue
Block a user