"""Who may edit what, read and written in one place. Disposable SQLite. The interesting cases are the ones a permissions screen gets wrong: a grant that silently means everything, a checkbox that changes nothing because a parent already covers it, and an article an educator can see but not edit in a branch they were given. """ import sys import unittest from types import ModuleType from unittest.mock import patch import test_quiz_builder as fixtures from app.models.article import Article from app.models.category_grant import CategoryGrant from app.models.media import MediaLibrary, MediaLibraryGrant from app.routers import access from app.utils.category_grants import can_edit_article, granted_category_scope with patch.dict(sys.modules, {"app.services.quiz_service": ModuleType("app.services.quiz_service")}): pass class AccessTests(unittest.TestCase): def setUp(self): self.bank = fixtures.BuilderTests() self.bank.setUp() self.client = self.bank.client self.db = self.bank.db self.client.app.include_router(access.router, prefix="/access") self.db.add(MediaLibrary(id=1, name="Rashes")) self.db.add(MediaLibrary(id=2, name="ECGs")) # Categories from the fixture: 1 Root > 2 Child > 3 Leaf, and 4 Empty. self.db.add(Article(id=50, slug="root-reading", title="Root reading", sections=[], category_id=2, user_id=3, status="published")) self.db.add(Article(id=51, slug="elsewhere", title="Elsewhere", sections=[], category_id=4, user_id=3, status="published")) self.db.commit() self.bank.user = self.bank.mod def tearDown(self): self.bank.tearDown() def peer(self): return self.bank.peer # ── reading ─────────────────────────────────────────────────────────────── def test_the_overview_says_what_each_person_actually_reaches(self): self.db.add(CategoryGrant(category_id=1, user_id=self.peer().id)) self.db.add(MediaLibraryGrant(library_id=1, user_id=self.peer().id)) self.db.commit() rows = {row["id"]: row for row in self.client.get("/access/").json()["users"]} row = rows[self.peer().id] self.assertEqual(row["categories"], [1]) self.assertEqual(row["libraries"], [1]) # Root, Child and Leaf — the branch, not just the row that was ticked. self.assertEqual(row["categories_covered"], 3) self.assertFalse(row["everything"]) self.assertTrue(rows[self.bank.mod.id]["everything"]) def test_the_tree_carries_what_is_in_each_branch(self): tree = self.client.get("/access/tree").json() by_id = {c["id"]: c for c in tree["categories"]} self.assertEqual(by_id[2]["articles"], 1) self.assertEqual({lib["name"] for lib in tree["libraries"]}, {"Rashes", "ECGs"}) def test_only_a_moderator_may_look(self): self.bank.user = self.peer() self.assertEqual(self.client.get("/access/").status_code, 403) self.assertEqual(self.client.get("/access/tree").status_code, 403) # ── writing ─────────────────────────────────────────────────────────────── def test_granting_a_branch_and_taking_it_back(self): uid = self.peer().id response = self.client.post(f"/access/{uid}/grants", json={"kind": "category", "target_id": 1}) self.assertEqual(response.status_code, 201, response.text) self.assertEqual(self.db.query(CategoryGrant).filter_by(user_id=uid).count(), 1) # Twice is refused rather than duplicated. self.assertEqual(self.client.post(f"/access/{uid}/grants", json={"kind": "category", "target_id": 1}).status_code, 409) self.assertEqual(self.client.delete(f"/access/{uid}/grants/category/1").status_code, 204) self.assertEqual(self.db.query(CategoryGrant).filter_by(user_id=uid).count(), 0) def test_a_grant_to_someone_who_already_has_everything_is_refused(self): response = self.client.post(f"/access/{self.bank.mod.id}/grants", json={"kind": "category", "target_id": 1}) self.assertEqual(response.status_code, 400) self.assertIn("already has everything", response.json()["detail"]) def test_everything_is_a_role_change_and_is_named_as_one(self): # With no provider mapping configured, the role is set here. The # patch is explicit because the container's own .env may have # OIDC_ROLE_CLAIM set, and a test must not read differently on two # machines. uid = self.peer().id with patch("app.services.sso_roles.is_configured", return_value=False): response = self.client.put(f"/access/{uid}/role", json={"role": "moderator"}) self.assertEqual(response.status_code, 200, response.text) self.db.expire_all() self.assertEqual(self.db.get(type(self.peer()), uid).role, "moderator") self.assertTrue({r["id"]: r for r in self.client.get("/access/").json()["users"]}[uid]["everything"]) def test_you_cannot_change_your_own_access(self): with patch("app.services.sso_roles.is_configured", return_value=False): self.assertEqual( self.client.put(f"/access/{self.bank.mod.id}/role", json={"role": "user"}).status_code, 400) def test_a_role_is_not_set_here_when_the_provider_holds_it(self): """A local change would be overwritten at their next sign-in. Which is the worst kind of setting: it appears to work, holds for an afternoon, and quietly undoes itself. The endpoint says where the switch actually is instead. """ with patch("app.services.sso_roles.is_configured", return_value=True): response = self.client.put(f"/access/{self.peer().id}/role", json={"role": "moderator"}) self.assertEqual(response.status_code, 409) self.assertIn("identity provider", response.json()["detail"]) def test_an_unknown_kind_is_refused_rather_than_ignored(self): uid = self.peer().id self.assertEqual(self.client.post(f"/access/{uid}/grants", json={"kind": "everything", "target_id": 1}).status_code, 400) self.assertEqual(self.client.delete(f"/access/{uid}/grants/everything/1").status_code, 400) # ── what a grant actually buys ──────────────────────────────────────────── def test_a_branch_covers_the_articles_filed_under_it(self): peer = self.peer() root = self.db.get(Article, 50) # category 2, inside the branch far = self.db.get(Article, 51) # category 4, outside it self.assertFalse(can_edit_article(self.db, peer, root)) self.db.add(CategoryGrant(category_id=1, user_id=peer.id)) self.db.commit() self.assertEqual(granted_category_scope(self.db, peer), {1, 2, 3}) # The reading in the branch, but not the reading outside it. self.assertTrue(can_edit_article(self.db, peer, root)) self.assertFalse(can_edit_article(self.db, peer, far)) def test_writing_an_article_does_not_make_it_yours(self): """Authorship was a way in. It is not one any more. An article belongs to the library, and who may change it is the admin role or a grant over the branch it is filed under — not whoever happened to type it. The address that wrote most of this library is not even the working administrator any more, which is the whole argument. """ own = Article(id=52, slug="mine", title="Mine", sections=[], category_id=4, user_id=self.peer().id, status="draft") self.db.add(own) self.db.commit() self.assertFalse(can_edit_article(self.db, self.peer(), own)) # A grant over where it is filed is a way in, and so is moderation. self.db.add(CategoryGrant(category_id=4, user_id=self.peer().id)) self.db.commit() self.assertTrue(can_edit_article(self.db, self.peer(), own)) self.assertTrue(can_edit_article(self.db, self.bank.mod, self.db.get(Article, 51))) def test_an_article_filed_nowhere_is_not_reachable_by_any_branch(self): loose = Article(id=53, slug="loose", title="Loose", sections=[], category_id=None, user_id=3, status="published") self.db.add(loose) self.db.add(CategoryGrant(category_id=1, user_id=self.peer().id)) self.db.commit() self.assertFalse(can_edit_article(self.db, self.peer(), loose)) if __name__ == "__main__": unittest.main()