""" 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