Files
the-other-dude/backend/tests/integration/test_devices_api.py
Jason Staack 06a41ca9bf fix(lint): resolve all ruff lint errors
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>
2026-03-14 22:17:50 -05:00

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