test_forum_security.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from unittest.mock import AsyncMock, patch
  2. from app.crud import create_user
  3. from app.schemas import UserCreate
  4. from starlette.websockets import WebSocketDisconnect
  5. def _login(client, username, password="password"):
  6. response = client.post(
  7. "/api/v1/auth/login",
  8. data={"username": username, "password": password},
  9. )
  10. assert response.status_code == 200
  11. return {"Authorization": f"Bearer {response.json()['access_token']}"}
  12. def _register(client, username):
  13. response = client.post(
  14. "/api/v1/auth/register",
  15. json={"username": username, "password": "password", "role": "admin"},
  16. )
  17. assert response.status_code == 200
  18. return response.json()
  19. def _create_forum(client, headers):
  20. persona = client.post(
  21. "/api/v1/personas/",
  22. headers=headers,
  23. json={"name": "Security Persona"},
  24. )
  25. assert persona.status_code == 200
  26. forum = client.post(
  27. "/api/v1/forums/",
  28. headers=headers,
  29. json={"topic": "Security Forum", "participant_ids": [persona.json()["id"]]},
  30. )
  31. assert forum.status_code == 200
  32. return forum.json()["id"]
  33. def test_public_registration_cannot_grant_admin(client):
  34. user = _register(client, "role-escalation")
  35. assert user["role"] == "user"
  36. def test_forum_resources_require_owner_or_admin(client, db):
  37. _register(client, "owner")
  38. _register(client, "intruder")
  39. owner_headers = _login(client, "owner")
  40. intruder_headers = _login(client, "intruder")
  41. forum_id = _create_forum(client, owner_headers)
  42. assert client.get(f"/api/v1/forums/{forum_id}").status_code == 401
  43. assert client.get(f"/api/v1/forums/{forum_id}/messages", headers=intruder_headers).status_code == 403
  44. assert client.get(f"/api/v1/forums/{forum_id}/logs", headers=intruder_headers).status_code == 403
  45. assert client.post(
  46. f"/api/v1/forums/{forum_id}/chat",
  47. headers=intruder_headers,
  48. json={"content": "unauthorized"},
  49. ).status_code == 403
  50. admin = create_user(db, UserCreate(username="admin", password="password", role="admin"))
  51. assert admin.role == "admin"
  52. admin_headers = _login(client, "admin")
  53. assert client.get(f"/api/v1/forums/{forum_id}", headers=admin_headers).status_code == 200
  54. def test_stop_forum_checks_ownership(client):
  55. _register(client, "stop-owner")
  56. _register(client, "stop-intruder")
  57. owner_headers = _login(client, "stop-owner")
  58. intruder_headers = _login(client, "stop-intruder")
  59. forum_id = _create_forum(client, owner_headers)
  60. assert client.post(f"/api/v1/forums/{forum_id}/stop", headers=intruder_headers).status_code == 403
  61. response = client.post(f"/api/v1/forums/{forum_id}/stop", headers=owner_headers)
  62. assert response.status_code == 200
  63. assert response.json() == {"status": "closed"}
  64. assert client.get(f"/api/v1/forums/{forum_id}", headers=owner_headers).json()["status"] == "closed"
  65. def test_forum_chat_rejects_blank_content(client):
  66. _register(client, "chat-owner")
  67. owner_headers = _login(client, "chat-owner")
  68. forum_id = _create_forum(client, owner_headers)
  69. response = client.post(
  70. f"/api/v1/forums/{forum_id}/chat",
  71. headers=owner_headers,
  72. json={"content": " "},
  73. )
  74. assert response.status_code == 400
  75. assert response.json()["detail"] == "Content is required"
  76. def test_websocket_requires_valid_owner_token(client):
  77. _register(client, "ws-owner")
  78. _register(client, "ws-intruder")
  79. owner_headers = _login(client, "ws-owner")
  80. intruder_headers = _login(client, "ws-intruder")
  81. forum_id = _create_forum(client, owner_headers)
  82. owner_token = owner_headers["Authorization"].removeprefix("Bearer ")
  83. intruder_token = intruder_headers["Authorization"].removeprefix("Bearer ")
  84. for path in (
  85. f"/api/v1/forums/{forum_id}/ws",
  86. f"/api/v1/forums/{forum_id}/ws?token=invalid",
  87. f"/api/v1/forums/{forum_id}/ws?token={intruder_token}",
  88. ):
  89. try:
  90. with client.websocket_connect(path) as websocket:
  91. websocket.receive_text()
  92. raise AssertionError("unauthorized websocket remained open")
  93. except WebSocketDisconnect as exc:
  94. assert exc.code == 1008
  95. with client.websocket_connect(f"/api/v1/forums/{forum_id}/ws?token={owner_token}") as websocket:
  96. websocket.send_text("ping")
  97. assert websocket.receive_text() == "pong"