test_groups_api.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Integration tests for the /api/v1/groups/* endpoints.
  2. Issue #1083: updates to a group's permission list must persist across GET,
  3. regardless of whether the frontend invalidates its React Query cache.
  4. """
  5. import pytest
  6. from httpx import AsyncClient
  7. from sqlalchemy import select
  8. from backend.app.models.group import Group
  9. async def _setup_admin(async_client: AsyncClient) -> dict[str, str]:
  10. await async_client.post(
  11. "/api/v1/auth/setup",
  12. json={"auth_enabled": True, "admin_username": "gadmin", "admin_password": "AdminPass1!"},
  13. )
  14. resp = await async_client.post(
  15. "/api/v1/auth/login",
  16. json={"username": "gadmin", "password": "AdminPass1!"},
  17. )
  18. return {"Authorization": f"Bearer {resp.json()['access_token']}"}
  19. @pytest.mark.asyncio
  20. @pytest.mark.integration
  21. async def test_update_group_permissions_persists(async_client: AsyncClient, db_session):
  22. """PATCH /groups/{id} with a new permissions list must persist to DB (#1083)."""
  23. headers = await _setup_admin(async_client)
  24. create = await async_client.post(
  25. "/api/v1/groups/",
  26. headers=headers,
  27. json={
  28. "name": "test_perms",
  29. "permissions": ["printers:read", "archives:read", "queue:read", "inventory:read"],
  30. },
  31. )
  32. assert create.status_code == 201
  33. gid = create.json()["id"]
  34. # Update to a wholly different set
  35. update = await async_client.patch(
  36. f"/api/v1/groups/{gid}",
  37. headers=headers,
  38. json={"permissions": ["users:read", "groups:read"]},
  39. )
  40. assert update.status_code == 200
  41. assert sorted(update.json()["permissions"]) == ["groups:read", "users:read"]
  42. # Re-read via API — must reflect the update, not the creation
  43. got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
  44. assert got.status_code == 200
  45. assert sorted(got.json()["permissions"]) == ["groups:read", "users:read"]
  46. # Direct DB read — same expectation
  47. result = await db_session.execute(select(Group).where(Group.id == gid))
  48. assert sorted(result.scalar_one().permissions or []) == ["groups:read", "users:read"]
  49. @pytest.mark.asyncio
  50. @pytest.mark.integration
  51. async def test_update_group_to_empty_permissions(async_client: AsyncClient, db_session):
  52. """Clearing all permissions via PATCH must result in an empty list, not a no-op."""
  53. headers = await _setup_admin(async_client)
  54. create = await async_client.post(
  55. "/api/v1/groups/",
  56. headers=headers,
  57. json={"name": "test_clear", "permissions": ["printers:read", "archives:read"]},
  58. )
  59. gid = create.json()["id"]
  60. update = await async_client.patch(
  61. f"/api/v1/groups/{gid}",
  62. headers=headers,
  63. json={"permissions": []},
  64. )
  65. assert update.status_code == 200
  66. assert update.json()["permissions"] == []
  67. got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
  68. assert got.json()["permissions"] == []
  69. @pytest.mark.asyncio
  70. @pytest.mark.integration
  71. async def test_update_group_without_permissions_field_preserves_existing(async_client: AsyncClient, db_session):
  72. """PATCH without a permissions field (None) must leave the existing list untouched."""
  73. headers = await _setup_admin(async_client)
  74. create = await async_client.post(
  75. "/api/v1/groups/",
  76. headers=headers,
  77. json={"name": "test_preserve", "permissions": ["printers:read", "archives:read"]},
  78. )
  79. gid = create.json()["id"]
  80. # Only update description
  81. update = await async_client.patch(
  82. f"/api/v1/groups/{gid}",
  83. headers=headers,
  84. json={"description": "updated"},
  85. )
  86. assert update.status_code == 200
  87. assert sorted(update.json()["permissions"]) == ["archives:read", "printers:read"]
  88. assert update.json()["description"] == "updated"
  89. @pytest.mark.asyncio
  90. @pytest.mark.integration
  91. async def test_update_group_invalid_permission_rejected(async_client: AsyncClient):
  92. """Invalid permission strings yield 400 and do not persist."""
  93. headers = await _setup_admin(async_client)
  94. create = await async_client.post(
  95. "/api/v1/groups/",
  96. headers=headers,
  97. json={"name": "test_bad", "permissions": ["printers:read"]},
  98. )
  99. gid = create.json()["id"]
  100. update = await async_client.patch(
  101. f"/api/v1/groups/{gid}",
  102. headers=headers,
  103. json={"permissions": ["printers:read", "bogus:permission"]},
  104. )
  105. assert update.status_code == 400
  106. assert "Invalid permissions" in update.json()["detail"]
  107. # Existing value unchanged
  108. got = await async_client.get(f"/api/v1/groups/{gid}", headers=headers)
  109. assert got.json()["permissions"] == ["printers:read"]