Add ruff config to exclude alembic E402, SQLAlchemy F821, and pre-existing E501 line-length issues. Auto-fix 69 unused imports and 2 f-strings without placeholders. Manually fix 8 unused variables. Apply ruff format to 127 files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
223 lines
7.2 KiB
Python
223 lines
7.2 KiB
Python
"""
|
|
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
|
|
|
|
|
|
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
|