test_collections_users_interaction.py 21 KB

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