|
|
@@ -0,0 +1,134 @@
|
|
|
+"""Integration tests for the /api/v1/groups/* endpoints.
|
|
|
+
|
|
|
+Issue #1083: updates to a group's permission list must persist across GET,
|
|
|
+regardless of whether the frontend invalidates its React Query cache.
|
|
|
+"""
|
|
|
+
|
|
|
+import pytest
|
|
|
+from httpx import AsyncClient
|
|
|
+from sqlalchemy import select
|
|
|
+
|
|
|
+from backend.app.models.group import Group
|
|
|
+
|
|
|
+
|
|
|
+async def _setup_admin(async_client: AsyncClient) -> dict[str, str]:
|
|
|
+ await async_client.post(
|
|
|
+ "/api/v1/auth/setup",
|
|
|
+ json={"auth_enabled": True, "admin_username": "gadmin", "admin_password": "AdminPass1!"},
|
|
|
+ )
|
|
|
+ resp = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "gadmin", "password": "AdminPass1!"},
|
|
|
+ )
|
|
|
+ return {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.integration
|
|
|
+async def test_update_group_permissions_persists(async_client: AsyncClient, db_session):
|
|
|
+ """PATCH /groups/{id} with a new permissions list must persist to DB (#1083)."""
|
|
|
+ headers = await _setup_admin(async_client)
|
|
|
+
|
|
|
+ create = await async_client.post(
|
|
|
+ "/api/v1/groups/",
|
|
|
+ headers=headers,
|
|
|
+ json={
|
|
|
+ "name": "test_perms",
|
|
|
+ "permissions": ["printers:read", "archives:read", "queue:read", "inventory:read"],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ assert create.status_code == 201
|
|
|
+ gid = create.json()["id"]
|
|
|
+
|
|
|
+ # Update to a wholly different set
|
|
|
+ update = await async_client.patch(
|
|
|
+ f"/api/v1/groups/{gid}",
|
|
|
+ headers=headers,
|
|
|
+ json={"permissions": ["users:read", "groups:read"]},
|
|
|
+ )
|
|
|
+ assert update.status_code == 200
|
|
|
+ assert sorted(update.json()["permissions"]) == ["groups:read", "users:read"]
|
|
|
+
|
|
|
+ # Re-read via API — must reflect the update, not the creation
|
|
|
+ got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
|
|
|
+ assert got.status_code == 200
|
|
|
+ assert sorted(got.json()["permissions"]) == ["groups:read", "users:read"]
|
|
|
+
|
|
|
+ # Direct DB read — same expectation
|
|
|
+ result = await db_session.execute(select(Group).where(Group.id == gid))
|
|
|
+ assert sorted(result.scalar_one().permissions or []) == ["groups:read", "users:read"]
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.integration
|
|
|
+async def test_update_group_to_empty_permissions(async_client: AsyncClient, db_session):
|
|
|
+ """Clearing all permissions via PATCH must result in an empty list, not a no-op."""
|
|
|
+ headers = await _setup_admin(async_client)
|
|
|
+
|
|
|
+ create = await async_client.post(
|
|
|
+ "/api/v1/groups/",
|
|
|
+ headers=headers,
|
|
|
+ json={"name": "test_clear", "permissions": ["printers:read", "archives:read"]},
|
|
|
+ )
|
|
|
+ gid = create.json()["id"]
|
|
|
+
|
|
|
+ update = await async_client.patch(
|
|
|
+ f"/api/v1/groups/{gid}",
|
|
|
+ headers=headers,
|
|
|
+ json={"permissions": []},
|
|
|
+ )
|
|
|
+ assert update.status_code == 200
|
|
|
+ assert update.json()["permissions"] == []
|
|
|
+
|
|
|
+ got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
|
|
|
+ assert got.json()["permissions"] == []
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.integration
|
|
|
+async def test_update_group_without_permissions_field_preserves_existing(async_client: AsyncClient, db_session):
|
|
|
+ """PATCH without a permissions field (None) must leave the existing list untouched."""
|
|
|
+ headers = await _setup_admin(async_client)
|
|
|
+
|
|
|
+ create = await async_client.post(
|
|
|
+ "/api/v1/groups/",
|
|
|
+ headers=headers,
|
|
|
+ json={"name": "test_preserve", "permissions": ["printers:read", "archives:read"]},
|
|
|
+ )
|
|
|
+ gid = create.json()["id"]
|
|
|
+
|
|
|
+ # Only update description
|
|
|
+ update = await async_client.patch(
|
|
|
+ f"/api/v1/groups/{gid}",
|
|
|
+ headers=headers,
|
|
|
+ json={"description": "updated"},
|
|
|
+ )
|
|
|
+ assert update.status_code == 200
|
|
|
+ assert sorted(update.json()["permissions"]) == ["archives:read", "printers:read"]
|
|
|
+ assert update.json()["description"] == "updated"
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.integration
|
|
|
+async def test_update_group_invalid_permission_rejected(async_client: AsyncClient):
|
|
|
+ """Invalid permission strings yield 400 and do not persist."""
|
|
|
+ headers = await _setup_admin(async_client)
|
|
|
+
|
|
|
+ create = await async_client.post(
|
|
|
+ "/api/v1/groups/",
|
|
|
+ headers=headers,
|
|
|
+ json={"name": "test_bad", "permissions": ["printers:read"]},
|
|
|
+ )
|
|
|
+ gid = create.json()["id"]
|
|
|
+
|
|
|
+ update = await async_client.patch(
|
|
|
+ f"/api/v1/groups/{gid}",
|
|
|
+ headers=headers,
|
|
|
+ json={"permissions": ["printers:read", "bogus:permission"]},
|
|
|
+ )
|
|
|
+ assert update.status_code == 400
|
|
|
+ assert "Invalid permissions" in update.json()["detail"]
|
|
|
+
|
|
|
+ # Existing value unchanged
|
|
|
+ got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
|
|
|
+ assert got.json()["permissions"] == ["printers:read"]
|