test_collections_users_interaction.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import uuid
  2. import pytest
  3. from r2r import R2RClient, R2RException
  4. # @pytest.fixture # (scope="session")
  5. # def client(config):
  6. # """A client logged in as a superuser."""
  7. # client = R2RClient(config.base_url)
  8. # client.users.login(config.superuser_email, config.superuser_password)
  9. # yield client
  10. @pytest.fixture
  11. def normal_user_client(mutable_client):
  12. """Create a normal user and log in with that user."""
  13. # client = R2RClient(config.base_url)
  14. email = f"normal_{uuid.uuid4()}@test.com"
  15. password = "normal_password"
  16. user_resp = mutable_client.users.create(email, password)
  17. mutable_client.users.login(email, password)
  18. yield mutable_client
  19. # Cleanup: Try deleting the normal user if exists
  20. try:
  21. mutable_client.users.login(email, password)
  22. mutable_client.users.delete(
  23. mutable_client.users.me()["results"]["id"], password
  24. )
  25. except R2RException:
  26. pass
  27. @pytest.fixture
  28. def another_normal_user_client(config):
  29. """Create another normal user and log in with that user."""
  30. client = R2RClient(config.base_url)
  31. email = f"another_{uuid.uuid4()}@test.com"
  32. password = "another_password"
  33. user_resp = client.users.create(email, password)
  34. client.users.login(email, password)
  35. yield client
  36. # Cleanup: Try deleting the user if exists
  37. try:
  38. client.users.login(email, password)
  39. client.users.delete(client.users.me()["results"]["id"], password)
  40. except R2RException:
  41. pass
  42. @pytest.fixture
  43. def user_owned_collection(normal_user_client):
  44. """Create a collection owned by the normal user."""
  45. resp = normal_user_client.collections.create(
  46. name="User Owned Collection",
  47. description="A collection owned by a normal user",
  48. )
  49. coll_id = resp["results"]["id"]
  50. yield coll_id
  51. # Cleanup
  52. try:
  53. normal_user_client.collections.delete(coll_id)
  54. except R2RException:
  55. pass
  56. @pytest.fixture
  57. def superuser_owned_collection(client):
  58. """Create a collection owned by the superuser."""
  59. resp = client.collections.create(
  60. name="Superuser Owned Collection",
  61. description="A collection owned by superuser",
  62. )
  63. coll_id = resp["results"]["id"]
  64. yield coll_id
  65. # Cleanup
  66. try:
  67. client.collections.delete(coll_id)
  68. except R2RException:
  69. pass
  70. def test_non_member_cannot_view_collection(
  71. normal_user_client, superuser_owned_collection
  72. ):
  73. """A normal user (not a member of a superuser-owned collection) tries to view it."""
  74. # The normal user is not added to the superuser collection, should fail
  75. with pytest.raises(R2RException) as exc_info:
  76. normal_user_client.collections.retrieve(superuser_owned_collection)
  77. assert (
  78. exc_info.value.status_code == 403
  79. ), "Non-member should not be able to view collection."
  80. def test_collection_owner_can_view_collection(
  81. normal_user_client, user_owned_collection
  82. ):
  83. """The owner should be able to view their own collection."""
  84. coll = normal_user_client.collections.retrieve(user_owned_collection)[
  85. "results"
  86. ]
  87. assert (
  88. coll["id"] == user_owned_collection
  89. ), "Owner cannot view their own collection."
  90. def test_collection_member_can_view_collection(
  91. client, normal_user_client, user_owned_collection
  92. ):
  93. """A user added to a collection should be able to view it."""
  94. # Create another user and add them to the user's collection
  95. new_user_email = f"temp_member_{uuid.uuid4()}@test.com"
  96. new_user_password = "temp_member_password"
  97. # Store normal user's email before any logouts
  98. normal_user_email = normal_user_client.users.me()["results"]["email"]
  99. # Create a new user and log in as them
  100. member_client = R2RClient(normal_user_client.base_url)
  101. member_client.users.create(new_user_email, new_user_password)
  102. member_client.users.login(new_user_email, new_user_password)
  103. member_id = member_client.users.me()["results"]["id"]
  104. # Owner adds the new user to the collection
  105. normal_user_client.users.logout()
  106. normal_user_client.users.login(normal_user_email, "normal_password")
  107. normal_user_client.collections.add_user(user_owned_collection, member_id)
  108. # The member now can view the collection
  109. coll = member_client.collections.retrieve(user_owned_collection)["results"]
  110. assert coll["id"] == user_owned_collection
  111. def test_non_owner_member_cannot_edit_collection(
  112. user_owned_collection, another_normal_user_client, normal_user_client
  113. ):
  114. """A member who is not the owner should not be able to edit the collection."""
  115. # Add another normal user to the owner's collection
  116. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  117. normal_user_client.collections.add_user(
  118. user_owned_collection, another_user_id
  119. )
  120. # Another normal user tries to update collection
  121. with pytest.raises(R2RException) as exc_info:
  122. another_normal_user_client.collections.update(
  123. user_owned_collection, name="Malicious Update"
  124. )
  125. assert (
  126. exc_info.value.status_code == 403
  127. ), "Non-owner member should not be able to edit."
  128. def test_non_owner_member_cannot_delete_collection(
  129. user_owned_collection, another_normal_user_client, normal_user_client
  130. ):
  131. """A member who is not the owner should not be able to delete the collection."""
  132. # Add the other user
  133. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  134. normal_user_client.collections.add_user(
  135. user_owned_collection, another_user_id
  136. )
  137. # Another user tries to delete
  138. with pytest.raises(R2RException) as exc_info:
  139. another_normal_user_client.collections.delete(user_owned_collection)
  140. assert (
  141. exc_info.value.status_code == 403
  142. ), "Non-owner member should not be able to delete."
  143. def test_non_owner_member_cannot_add_other_users(
  144. user_owned_collection, another_normal_user_client, normal_user_client
  145. ):
  146. """A member who is not the owner should not be able to add other users."""
  147. # Another user tries to add a third user
  148. third_email = f"third_user_{uuid.uuid4()}@test.com"
  149. third_password = "third_password"
  150. # Need to create third user as a superuser or owner
  151. normal_user_email = normal_user_client.users.me()["results"]["email"]
  152. normal_user_client.users.logout()
  153. # Login as normal user again
  154. # NOTE: We assume normal_password known here; in a real scenario, store it or use fixtures more dynamically
  155. # This code snippet assumes we have these credentials available.
  156. # If not, manage credentials store in fixture creation.
  157. normal_user_client.users.login(normal_user_email, "normal_password")
  158. third_user_resp = normal_user_client.users.create(
  159. third_email, third_password
  160. )
  161. third_user_id = third_user_resp["results"]["id"]
  162. # Add another user as a member
  163. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  164. normal_user_client.collections.add_user(
  165. user_owned_collection, another_user_id
  166. )
  167. # Now, another_normal_user_client tries to add the third user
  168. with pytest.raises(R2RException) as exc_info:
  169. another_normal_user_client.collections.add_user(
  170. user_owned_collection, third_user_id
  171. )
  172. assert (
  173. exc_info.value.status_code == 403
  174. ), "Non-owner member should not be able to add users."
  175. def test_owner_can_remove_member_from_collection(
  176. user_owned_collection, another_normal_user_client, normal_user_client
  177. ):
  178. """The owner should be able to remove a member from their collection."""
  179. # Add another user to the collection
  180. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  181. normal_user_client.collections.add_user(
  182. user_owned_collection, another_user_id
  183. )
  184. # Remove them
  185. remove_resp = normal_user_client.collections.remove_user(
  186. user_owned_collection, another_user_id
  187. )["results"]
  188. assert remove_resp["success"], "Owner could not remove member."
  189. # The removed user should no longer have access
  190. with pytest.raises(R2RException) as exc_info:
  191. another_normal_user_client.collections.retrieve(user_owned_collection)
  192. assert (
  193. exc_info.value.status_code == 403
  194. ), "Removed user still has access after removal."
  195. def test_superuser_can_access_any_collection(client, user_owned_collection):
  196. """A superuser should be able to view and edit any collection."""
  197. # Superuser can view
  198. coll = client.collections.retrieve(user_owned_collection)["results"]
  199. assert (
  200. coll["id"] == user_owned_collection
  201. ), "Superuser cannot view a user collection."
  202. # Superuser can also update
  203. updated = client.collections.update(
  204. user_owned_collection, name="Superuser Edit"
  205. )["results"]
  206. assert (
  207. updated["name"] == "Superuser Edit"
  208. ), "Superuser cannot edit collection."
  209. def test_unauthenticated_cannot_access_collections(
  210. config, user_owned_collection
  211. ):
  212. """An unauthenticated (no login) client should not access protected endpoints."""
  213. unauth_client = R2RClient(config.base_url)
  214. # we must CREATE + LOGIN as superuser is default user for unauth in basic config
  215. user_name = f"unauth_user_{uuid.uuid4()}@email.com"
  216. unauth_client.users.create(user_name, "unauth_password")
  217. unauth_client.users.login(user_name, "unauth_password")
  218. with pytest.raises(R2RException) as exc_info:
  219. unauth_client.collections.retrieve(user_owned_collection)
  220. assert (
  221. exc_info.value.status_code == 403
  222. ), "Unaurthorized user should get 403"
  223. def test_user_cannot_add_document_to_collection_they_cannot_edit(
  224. client, normal_user_client
  225. ):
  226. """A normal user who is just a member (not owner) of a collection should not be able to add documents."""
  227. # Create a collection as normal user (owner)
  228. resp = normal_user_client.collections.create(
  229. name="Owned by user", description="desc"
  230. )
  231. coll_id = resp["results"]["id"]
  232. # Create a second user and add them as member
  233. second_email = f"second_{uuid.uuid4()}@test.com"
  234. second_password = "pwd"
  235. client.users.logout()
  236. second_client = R2RClient(normal_user_client.base_url)
  237. second_client.users.create(second_email, second_password)
  238. second_client.users.login(second_email, second_password)
  239. second_id = second_client.users.me()["results"]["id"]
  240. # Owner adds second user as a member
  241. email_of_normal_user = normal_user_client.users.me()["results"]["email"]
  242. normal_user_client.users.logout()
  243. # Re-login owner (assuming we stored the original user's creds)
  244. # For demonstration, we assume we know the normal_user_client creds or re-use fixtures carefully.
  245. # In a real test environment, you'd maintain credentials more robustly.
  246. # Here we rely on the normal_user_client fixture being re-instantiated per test if needed.
  247. normal_user_client.users.login(email_of_normal_user, "normal_password")
  248. normal_user_client.collections.add_user(coll_id, second_id)
  249. # Create a document as owner
  250. doc_resp = normal_user_client.documents.create(raw_text="Test Document")
  251. doc_id = doc_resp["results"]["document_id"]
  252. # Now second user tries to add another document (which they do not have edit rights for)
  253. second_client.users.logout()
  254. second_client.users.login(second_email, second_password)
  255. # Another doc created by second user (just for attempt)
  256. doc2_resp = second_client.documents.create(raw_text="Doc by second user")
  257. doc2_id = doc2_resp["results"]["document_id"]
  258. # Second user tries to add their doc2_id to the owner’s collection
  259. with pytest.raises(R2RException) as exc_info:
  260. second_client.collections.add_document(coll_id, doc2_id)
  261. assert (
  262. exc_info.value.status_code == 403
  263. ), "Non-owner member should not add documents."
  264. # Cleanup
  265. normal_user_client.collections.delete(coll_id)
  266. def test_user_cannot_remove_document_from_collection_they_cannot_edit(
  267. normal_user_client,
  268. ):
  269. """A user who is just a member should not remove documents."""
  270. # Create a collection
  271. resp = normal_user_client.collections.create(
  272. name="Removable", description="desc"
  273. )
  274. coll_id = resp["results"]["id"]
  275. # Create a document in it
  276. doc_resp = normal_user_client.documents.create(raw_text="Doc in coll")
  277. doc_id = doc_resp["results"]["document_id"]
  278. normal_user_client.collections.add_document(coll_id, doc_id)
  279. # Create another user and add as member
  280. another_email = f"amember_{uuid.uuid4()}@test.com"
  281. another_password = "memberpwd"
  282. member_client = R2RClient(normal_user_client.base_url)
  283. member_client.users.create(another_email, another_password)
  284. member_client.users.login(another_email, another_password)
  285. member_id = member_client.users.me()["results"]["id"]
  286. user_email = normal_user_client.users.me()["results"]["email"]
  287. # Add member to collection
  288. normal_user_client.users.logout()
  289. normal_user_client.users.login(user_email, "normal_password")
  290. normal_user_client.collections.add_user(coll_id, member_id)
  291. # Member tries to remove the document
  292. with pytest.raises(R2RException) as exc_info:
  293. member_client.collections.remove_document(coll_id, doc_id)
  294. assert (
  295. exc_info.value.status_code == 403
  296. ), "Member should not remove documents."
  297. # Cleanup
  298. normal_user_client.collections.delete(coll_id)
  299. def test_normal_user_cannot_make_another_user_superuser(normal_user_client):
  300. """A normal user tries to update another user to superuser, should fail."""
  301. # Create another user
  302. email = f"regular_{uuid.uuid4()}@test.com"
  303. password = "not_superuser"
  304. new_user_resp = normal_user_client.users.create(email, password)
  305. new_user_id = new_user_resp["results"]["id"]
  306. # Try updating their superuser status
  307. with pytest.raises(R2RException) as exc_info:
  308. normal_user_client.users.update(new_user_id, is_superuser=True)
  309. assert (
  310. exc_info.value.status_code == 403
  311. ), "Non-superuser should not grant superuser status."
  312. def test_normal_user_cannot_view_other_users_if_not_superuser(
  313. normal_user_client,
  314. ):
  315. """A normal user tries to list all users, should fail."""
  316. with pytest.raises(R2RException) as exc_info:
  317. normal_user_client.users.list()
  318. assert (
  319. exc_info.value.status_code == 403
  320. ), "Non-superuser should not list all users."
  321. def test_normal_user_cannot_update_other_users_details(
  322. normal_user_client, client
  323. ):
  324. """A normal user tries to update another normal user's details."""
  325. # Create another normal user
  326. email = f"other_normal_{uuid.uuid4()}@test.com"
  327. password = "pwd123"
  328. client.users.logout()
  329. another_client = R2RClient(normal_user_client.base_url)
  330. another_client.users.create(email, password)
  331. another_client.users.login(email, password)
  332. another_user_id = another_client.users.me()["results"]["id"]
  333. another_client.users.logout()
  334. # Try to update as first normal user (not superuser, not same user)
  335. with pytest.raises(R2RException) as exc_info:
  336. normal_user_client.users.update(another_user_id, name="Hacked Name")
  337. assert (
  338. exc_info.value.status_code == 403
  339. ), "Non-superuser should not update another user's info."
  340. # Additional Tests for Strengthened Coverage
  341. def test_owner_cannot_promote_member_to_superuser_via_collection(
  342. user_owned_collection, normal_user_client, another_normal_user_client
  343. ):
  344. """
  345. Ensures that being a collection owner doesn't confer the right
  346. to promote a user to superuser.
  347. """
  348. # Add another user to the collection
  349. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  350. normal_user_client.collections.add_user(
  351. user_owned_collection, another_user_id
  352. )
  353. # Try to update the member's superuser status
  354. with pytest.raises(R2RException) as exc_info:
  355. normal_user_client.users.update(another_user_id, is_superuser=True)
  356. assert (
  357. exc_info.value.status_code == 403
  358. ), "Collection owners should not grant superuser status."
  359. def test_member_cannot_view_other_users_info(
  360. user_owned_collection, normal_user_client, another_normal_user_client
  361. ):
  362. """
  363. A member (non-owner) of a collection should not be able to retrieve other users' details
  364. outside of their allowed scope.
  365. """
  366. # Add the other normal user as a member
  367. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  368. normal_user_client.collections.add_user(
  369. user_owned_collection, another_user_id
  370. )
  371. # As another_normal_user_client (a member), try to retrieve owner user details
  372. owner_id = normal_user_client.users.me()["results"]["id"]
  373. with pytest.raises(R2RException) as exc_info:
  374. another_normal_user_client.users.retrieve(owner_id)
  375. assert (
  376. exc_info.value.status_code == 403
  377. ), "Members should not be able to view other users' details."
  378. def test_unauthenticated_user_cannot_join_collection(
  379. config, user_owned_collection
  380. ):
  381. """
  382. An unauthenticated user should not be able to join or view collections.
  383. """
  384. unauth_client = R2RClient(config.base_url)
  385. # we must CREATE + LOGIN as superuser is default user for unauth in basic config
  386. user_name = f"unauth_user_{uuid.uuid4()}@email.com"
  387. unauth_client.users.create(user_name, "unauth_password")
  388. unauth_client.users.login(user_name, "unauth_password")
  389. # No login performed here, client is unauthenticated
  390. with pytest.raises(R2RException) as exc_info:
  391. unauth_client.collections.retrieve(user_owned_collection)
  392. assert exc_info.value.status_code in [
  393. 401,
  394. 403,
  395. ], "Unauthenticated user should not access collections."
  396. def test_non_owner_cannot_remove_users_they_did_not_add(
  397. user_owned_collection, normal_user_client, another_normal_user_client
  398. ):
  399. """
  400. A member who is not the owner cannot remove other members from the collection.
  401. """
  402. # Add another user as a member
  403. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  404. normal_user_client.collections.add_user(
  405. user_owned_collection, another_user_id
  406. )
  407. # Now try removing that user as another_normal_user_client
  408. with pytest.raises(R2RException) as exc_info:
  409. another_normal_user_client.collections.remove_user(
  410. user_owned_collection, another_user_id
  411. )
  412. assert (
  413. exc_info.value.status_code == 403
  414. ), "Non-owner member should not remove other users."
  415. def test_owner_cannot_access_deleted_member_info_after_removal(
  416. user_owned_collection, normal_user_client, another_normal_user_client
  417. ):
  418. """
  419. After the owner removes a user from the collection, ensure that attempts to
  420. perform collection-specific actions with that user fail.
  421. """
  422. # Add another user to the collection
  423. another_user_id = another_normal_user_client.users.me()["results"]["id"]
  424. normal_user_client.collections.add_user(
  425. user_owned_collection, another_user_id
  426. )
  427. # Remove them
  428. normal_user_client.collections.remove_user(
  429. user_owned_collection, another_user_id
  430. )
  431. # Now, try listing collections for that removed user (as owner),
  432. # if there's an endpoint that filters by user, to ensure no special access remains.
  433. # If no such endpoint exists, this test can be adapted to try another relevant action.
  434. # For demonstration, we might attempt to retrieve user details as owner:
  435. with pytest.raises(R2RException) as exc_info:
  436. normal_user_client.users.retrieve(another_user_id)
  437. # We expect a 403 because normal_user_client is not superuser and not that user.
  438. assert (
  439. exc_info.value.status_code == 403
  440. ), "Owner should not access removed member's user info."
  441. def test_member_cannot_add_document_to_non_existent_collection(
  442. normal_user_client,
  443. ):
  444. """
  445. A member tries to add a document to a collection that doesn't exist.
  446. """
  447. fake_coll_id = str(uuid.uuid4())
  448. doc_resp = normal_user_client.documents.create(raw_text="Test Doc")
  449. doc_id = doc_resp["results"]["document_id"]
  450. with pytest.raises(R2RException) as exc_info:
  451. normal_user_client.collections.add_document(fake_coll_id, doc_id)
  452. assert exc_info.value.status_code in [
  453. 400,
  454. 404,
  455. ], "Expected error when adding doc to non-existent collection."