From cb1aa8bef0a3e151102d710e794c228ece863b7c Mon Sep 17 00:00:00 2001 From: thomas girod Date: Mon, 22 Jul 2024 19:12:03 +0200 Subject: [PATCH] add tests --- core/baker_recipes.py | 32 +++++ pedagogy/api.py | 7 +- pedagogy/schemas.py | 2 +- pedagogy/templates/pedagogy/guide.jinja | 2 - pedagogy/tests/__init__.py | 0 pedagogy/tests/test_api.py | 165 ++++++++++++++++++++++++ pedagogy/{ => tests}/tests.py | 152 ---------------------- poetry.lock | 20 ++- pyproject.toml | 1 + sas/api.py | 8 +- sas/tests/__init__.py | 0 sas/tests/test_api.py | 103 +++++++++++++++ sas/{ => tests}/tests.py | 0 13 files changed, 332 insertions(+), 160 deletions(-) create mode 100644 core/baker_recipes.py create mode 100644 pedagogy/tests/__init__.py create mode 100644 pedagogy/tests/test_api.py rename pedagogy/{ => tests}/tests.py (84%) create mode 100644 sas/tests/__init__.py create mode 100644 sas/tests/test_api.py rename sas/{ => tests}/tests.py (100%) diff --git a/core/baker_recipes.py b/core/baker_recipes.py new file mode 100644 index 00000000..5736efa9 --- /dev/null +++ b/core/baker_recipes.py @@ -0,0 +1,32 @@ +from datetime import timedelta + +from django.utils.timezone import now +from model_bakery import seq +from model_bakery.recipe import Recipe, related + +from core.models import User +from subscription.models import Subscription + +active_subscription = Recipe( + Subscription, + subscription_start=now() - timedelta(days=30), + subscription_end=now() + timedelta(days=30), +) +ended_subscription = Recipe( + Subscription, + subscription_start=now() - timedelta(days=60), + subscription_end=now() - timedelta(days=30), +) + +subscriber_user = Recipe( + User, + first_name="subscriber", + last_name=seq("user "), + subscriptions=related(active_subscription), +) +old_subscriber_user = Recipe( + User, + first_name="old subscriber", + last_name=seq("user "), + subscriptions=related(ended_subscription), +) diff --git a/pedagogy/api.py b/pedagogy/api.py index 5bb359be..d7a8d457 100644 --- a/pedagogy/api.py +++ b/pedagogy/api.py @@ -13,7 +13,7 @@ from pedagogy.schemas import SimpleUvSchema, UvFilterSchema, UvSchema from pedagogy.utbm_api import find_uv -@api_controller("/uv", permissions=[IsSubscriber]) +@api_controller("/uv") class UvController(ControllerBase): @route.get( "/{year}/{code}", @@ -31,7 +31,10 @@ class UvController(ControllerBase): return res @route.get( - "", response=PaginatedResponseSchema[SimpleUvSchema], url_name="fetch_uvs" + "", + response=PaginatedResponseSchema[SimpleUvSchema], + url_name="fetch_uvs", + permissions=[IsSubscriber | IsInGroup(settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)], ) @paginate(PageNumberPaginationExtra, page_size=100) def fetch_uv_list(self, search: Query[UvFilterSchema]): diff --git a/pedagogy/schemas.py b/pedagogy/schemas.py index 5ce120fd..9729aa6d 100644 --- a/pedagogy/schemas.py +++ b/pedagogy/schemas.py @@ -123,7 +123,7 @@ class UvFilterSchema(FilterSchema): def filter_semester(self, value: set[str] | None) -> Q: """Special filter for the semester. - If both "SPRING" and "AUTUMN" are given, UV that are available + If either "SPRING" or "AUTUMN" is given, UV that are available during "AUTUMN_AND_SPRING" will be filtered. """ if not value: diff --git a/pedagogy/templates/pedagogy/guide.jinja b/pedagogy/templates/pedagogy/guide.jinja index e494492c..13012240 100644 --- a/pedagogy/templates/pedagogy/guide.jinja +++ b/pedagogy/templates/pedagogy/guide.jinja @@ -125,8 +125,6 @@ function update_query_string(key, value) { const url = new URL(window.location.href); - console.log(value) - console.log(!!value) if (!value) { url.searchParams.delete(key) } else if (Array.isArray(value)) { diff --git a/pedagogy/tests/__init__.py b/pedagogy/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py new file mode 100644 index 00000000..ad58605b --- /dev/null +++ b/pedagogy/tests/test_api.py @@ -0,0 +1,165 @@ +import json + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from model_bakery import baker +from model_bakery.recipe import Recipe + +from core.baker_recipes import subscriber_user +from core.models import RealGroup, User +from pedagogy.models import UV + + +class UVSearchTest(TestCase): + """Test UV guide rights for view and API.""" + + @classmethod + def setUpTestData(cls): + cls.root = User.objects.get(username="root") + cls.url = reverse("api:fetch_uvs") + uv_recipe = Recipe(UV, author=cls.root) + uvs = [ + uv_recipe.prepare( + code="AP4A", credit_type="CS", semester="AUTUMN", department="GI" + ), + uv_recipe.prepare( + code="MT01", credit_type="CS", semester="AUTUMN", department="TC" + ), + uv_recipe.prepare( + code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC" + ), + uv_recipe.prepare( + code="TNEV", credit_type="TM", semester="SPRING", department="TC" + ), + uv_recipe.prepare( + code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI" + ), + uv_recipe.prepare( + code="DA50", + credit_type="TM", + semester="AUTUMN_AND_SPRING", + department="GI", + ), + ] + UV.objects.bulk_create(uvs) + + def test_permissions(self): + # Test with anonymous user + response = self.client.get(self.url) + assert response.status_code == 403 + + # Test with not subscribed user + self.client.force_login(baker.make(User)) + response = self.client.get(self.url) + assert response.status_code == 403 + + for user in ( + self.root, + subscriber_user.make(), + baker.make( + User, + groups=[ + RealGroup.objects.get(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID) + ], + ), + ): + # users that have right + with self.subTest(): + self.client.force_login(user) + response = self.client.get(self.url) + assert response.status_code == 200 + + def test_format(self): + """Test that the return data format is correct""" + self.client.force_login(self.root) + res = self.client.get(self.url + "?search=PA00") + uv = UV.objects.get(code="PA00") + assert res.status_code == 200 + assert json.loads(res.content) == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": uv.id, + "title": uv.title, + "code": uv.code, + "credit_type": uv.credit_type, + "semester": uv.semester, + "department": uv.department, + } + ], + } + + def test_search_by_code(self): + self.client.force_login(self.root) + res = self.client.get(self.url + "?search=MT") + assert res.status_code == 200 + assert {uv["code"] for uv in json.loads(res.content)["results"]} == { + "MT01", + "MT10", + } + + def test_search_by_credit_type(self): + self.client.force_login(self.root) + res = self.client.get(self.url + "?credit_type=CS") + assert res.status_code == 200 + codes = [uv["code"] for uv in json.loads(res.content)["results"]] + assert codes == ["AP4A", "MT01", "PHYS11"] + res = self.client.get(self.url + "?credit_type=CS&credit_type=OM") + assert res.status_code == 200 + codes = {uv["code"] for uv in json.loads(res.content)["results"]} + assert codes == {"AP4A", "MT01", "PHYS11", "PA00"} + + def test_search_by_semester(self): + self.client.force_login(self.root) + res = self.client.get(self.url + "?semester=SPRING") + assert res.status_code == 200 + codes = {uv["code"] for uv in json.loads(res.content)["results"]} + assert codes == {"DA50", "TNEV", "PA00"} + + def test_search_multiple_filters(self): + self.client.force_login(self.root) + res = self.client.get( + self.url + "?semester=AUTUMN&credit_type=CS&department=TC" + ) + assert res.status_code == 200 + codes = {uv["code"] for uv in json.loads(res.content)["results"]} + assert codes == {"MT01", "PHYS11"} + + def test_search_fails(self): + self.client.force_login(self.root) + res = self.client.get(self.url + "?credit_type=CS&search=DA") + assert res.status_code == 200 + assert json.loads(res.content)["results"] == [] + + def test_search_pa00_fail(self): + self.client.force_login(self.root) + # Search with UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"}) + self.assertNotContains(response, text="PA00") + + # Search with first letter of UV code + response = self.client.get(reverse("pedagogy:guide"), {"search": "I"}) + self.assertNotContains(response, text="PA00") + + # Search with UV manager + response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"}) + self.assertNotContains(response, text="PA00") + + # Search with department + response = self.client.get(reverse("pedagogy:guide"), {"department": "TC"}) + self.assertNotContains(response, text="PA00") + + # Search with semester + response = self.client.get(reverse("pedagogy:guide"), {"semester": "CLOSED"}) + self.assertNotContains(response, text="PA00") + + # Search with language + response = self.client.get(reverse("pedagogy:guide"), {"language": "EN"}) + self.assertNotContains(response, text="PA00") + + # Search with credit type + response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "TM"}) + self.assertNotContains(response, text="PA00") diff --git a/pedagogy/tests.py b/pedagogy/tests/tests.py similarity index 84% rename from pedagogy/tests.py rename to pedagogy/tests/tests.py index 3b97ed62..018187b2 100644 --- a/pedagogy/tests.py +++ b/pedagogy/tests/tests.py @@ -20,7 +20,6 @@ # Place - Suite 330, Boston, MA 02111-1307, USA. # # -import json import pytest from django.conf import settings @@ -555,157 +554,6 @@ class UVCommentUpdateTest(TestCase): self.assertEqual(self.comment.author, self.krophil) -class UVSearchTest(TestCase): - """Test UV guide rights for view and API.""" - - @classmethod - def setUpTestData(cls): - cls.bibou = User.objects.get(username="root") - cls.tutu = User.objects.get(username="tutu") - cls.sli = User.objects.get(username="sli") - cls.guy = User.objects.get(username="guy") - cls.url = reverse("api:fetch_uvs") - uvs = [ - UV(code="AP4A", credit_type="CS", semester="AUTUMN", department="GI"), - UV(code="MT01", credit_type="CS", semester="AUTUMN", department="TC"), - UV(code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC"), - UV(code="TNEV", credit_type="TM", semester="SPRING", department="TC"), - UV(code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI"), - UV( - code="DA50", - credit_type="TM", - semester="AUTUMN_AND_SPRING", - department="GI", - ), - ] - for uv in uvs: - uv.author = cls.bibou - uv.title = "" - uv.manager = "" - uv.language = "FR" - uv.objectives = "" - uv.program = "" - uv.skills = "" - uv.key_concepts = "" - uv.credits = 6 - UV.objects.bulk_create(uvs) - - def fetch_uvs(self, **kwargs): - params = "&".join(f"{key}={val}" for key, val in kwargs.items()) - return json.loads(f"{self.url}?{params}") - - def test_permissions(self): - # Test with anonymous user - response = self.client.get(self.url) - assert response.status_code == 403 - - # Test with not subscribed user - self.client.force_login(self.guy) - response = self.client.get(self.url) - assert response.status_code == 403 - - for user in self.bibou, self.tutu, self.sli: - # users that have right - with self.subTest(): - self.client.force_login(user) - response = self.client.get(self.url) - assert response.status_code == 200 - - def test_format(self): - """Test that the return data format is correct""" - self.client.force_login(self.bibou) - res = self.client.get(self.url + "?search=PA00") - uv = UV.objects.get(code="PA00") - assert res.status_code == 200 - assert json.loads(res.content) == { - "count": 1, - "next": None, - "previous": None, - "results": [ - { - "id": uv.id, - "title": uv.title, - "code": uv.code, - "credit_type": uv.credit_type, - "semester": uv.semester, - "department": uv.department, - } - ], - } - - def test_search_by_code(self): - self.client.force_login(self.bibou) - res = self.client.get(self.url + "?search=MT") - assert res.status_code == 200 - assert {uv["code"] for uv in json.loads(res.content)["results"]} == { - "MT01", - "MT10", - } - - def test_search_by_credit_type(self): - self.client.force_login(self.bibou) - res = self.client.get(self.url + "?credit_type=CS") - assert res.status_code == 200 - codes = [uv["code"] for uv in json.loads(res.content)["results"]] - assert codes == ["AP4A", "MT01", "PHYS11"] - res = self.client.get(self.url + "?credit_type=CS&credit_type=OM") - assert res.status_code == 200 - codes = {uv["code"] for uv in json.loads(res.content)["results"]} - assert codes == {"AP4A", "MT01", "PHYS11", "PA00"} - - def test_search_by_semester(self): - self.client.force_login(self.bibou) - res = self.client.get(self.url + "?semester=SPRING") - assert res.status_code == 200 - codes = {uv["code"] for uv in json.loads(res.content)["results"]} - assert codes == {"DA50", "TNEV", "PA00"} - - def test_search_multiple_filters(self): - self.client.force_login(self.bibou) - res = self.client.get( - self.url + "?semester=AUTUMN&credit_type=CS&department=TC" - ) - assert res.status_code == 200 - codes = {uv["code"] for uv in json.loads(res.content)["results"]} - assert codes == {"MT01", "PHYS11"} - - def test_search_fails(self): - self.client.force_login(self.bibou) - res = self.client.get(self.url + "?credit_type=CS&search=DA") - assert res.status_code == 200 - assert json.loads(res.content)["results"] == [] - - def test_search_pa00_fail(self): - self.client.force_login(self.bibou) - # Search with UV code - response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"}) - self.assertNotContains(response, text="PA00") - - # Search with first letter of UV code - response = self.client.get(reverse("pedagogy:guide"), {"search": "I"}) - self.assertNotContains(response, text="PA00") - - # Search with UV manager - response = self.client.get(reverse("pedagogy:guide"), {"search": "GILLES"}) - self.assertNotContains(response, text="PA00") - - # Search with department - response = self.client.get(reverse("pedagogy:guide"), {"department": "TC"}) - self.assertNotContains(response, text="PA00") - - # Search with semester - response = self.client.get(reverse("pedagogy:guide"), {"semester": "CLOSED"}) - self.assertNotContains(response, text="PA00") - - # Search with language - response = self.client.get(reverse("pedagogy:guide"), {"language": "EN"}) - self.assertNotContains(response, text="PA00") - - # Search with credit type - response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "TM"}) - self.assertNotContains(response, text="PA00") - - class UVModerationFormTest(TestCase): """Assert access rights and if the form works well.""" diff --git a/poetry.lock b/poetry.lock index 227ddc06..d4c9c8df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1225,6 +1225,24 @@ files = [ griffe = ">=0.47" mkdocstrings = ">=0.25" +[[package]] +name = "model-bakery" +version = "1.18.2" +description = "Smart object creation facility for Django." +optional = false +python-versions = ">=3.8" +files = [ + {file = "model_bakery-1.18.2-py3-none-any.whl", hash = "sha256:fd13a251d20db78b790d80f75350a73af5d199e5151227b5dd35cb76f2f08fe8"}, + {file = "model_bakery-1.18.2.tar.gz", hash = "sha256:8f8ab4ba26a206ed848da9b1740b5006b5eeca8a67389efb28dbff37b362e802"}, +] + +[package.dependencies] +django = ">=4.2" + +[package.extras] +docs = ["myst-parser", "sphinx", "sphinx-rtd-theme"] +test = ["black", "coverage", "mypy", "pillow", "pytest", "pytest-django", "ruff"] + [[package]] name = "nodeenv" version = "1.9.1" @@ -2454,4 +2472,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "ee0b881719f6834880266d72272429708e781b3ccd34a0fbf3e8b4119dcb95fd" +content-hash = "f8e48947d004d61d63a345d36d7b42777030e1ac3687bb27f97b2c51318fcc8d" diff --git a/pyproject.toml b/pyproject.toml index 2d26e082..f77bd055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ freezegun = "^1.2.2" # used to test time-dependent code pytest = "^8.2.2" pytest-cov = "^5.0.0" pytest-django = "^4.8.0" +model-bakery = "^1.18.2" # deps used to work on the documentation [tool.poetry.group.docs.dependencies] diff --git a/sas/api.py b/sas/api.py index babc119e..c1159df7 100644 --- a/sas/api.py +++ b/sas/api.py @@ -33,8 +33,12 @@ class SasController(ControllerBase): # User can view any moderated picture if he/she is subscribed. # If not, he/she can view only the one he/she has been identified on raise PermissionDenied - pictures = filters.filter( - Picture.objects.filter(is_moderated=True, asked_for_removal=False) + pictures = list( + filters.filter( + Picture.objects.filter(is_moderated=True, asked_for_removal=False) + ) + .distinct() + .order_by("-date") ) for picture in pictures: picture.full_size_url = picture.get_download_url() diff --git a/sas/tests/__init__.py b/sas/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py new file mode 100644 index 00000000..71b5f82c --- /dev/null +++ b/sas/tests/test_api.py @@ -0,0 +1,103 @@ +from django.test import TestCase +from django.urls import reverse +from model_bakery import baker +from model_bakery.recipe import Recipe + +from core.baker_recipes import old_subscriber_user, subscriber_user +from core.models import User +from sas.models import Album, PeoplePictureRelation, Picture + + +class SasTest(TestCase): + @classmethod + def setUpTestData(cls): + Picture.objects.all().delete() + owner = User.objects.get(username="root") + + cls.user_a = old_subscriber_user.make() + cls.user_b, cls.user_c = subscriber_user.make(_quantity=2) + + picture_recipe = Recipe( + Picture, is_in_sas=True, is_folder=False, owner=owner, is_moderated=True + ) + cls.album_a = baker.make(Album, is_in_sas=True) + cls.album_b = baker.make(Album, is_in_sas=True) + for album in cls.album_a, cls.album_b: + pictures = picture_recipe.make(parent=album, _quantity=5, _bulk_create=True) + baker.make(PeoplePictureRelation, picture=pictures[1], user=cls.user_a) + baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_a) + baker.make(PeoplePictureRelation, picture=pictures[2], user=cls.user_b) + baker.make(PeoplePictureRelation, picture=pictures[3], user=cls.user_b) + baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_a) + baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_b) + baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_c) + + def test_anonymous_user_forbidden(self): + res = self.client.get(reverse("api:pictures")) + assert res.status_code == 403 + + def test_filter_by_album(self): + self.client.force_login(self.user_b) + res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}") + assert res.status_code == 200 + expected = list( + self.album_a.children_pictures.order_by("-date").values_list( + "id", flat=True + ) + ) + assert [i["id"] for i in res.json()] == expected + + def test_filter_by_user(self): + self.client.force_login(self.user_b) + res = self.client.get( + reverse("api:pictures") + f"?users_identified={self.user_a.id}" + ) + assert res.status_code == 200 + expected = list( + self.user_a.pictures.order_by("-picture__date").values_list( + "picture_id", flat=True + ) + ) + assert [i["id"] for i in res.json()] == expected + + def test_filter_by_multiple_user(self): + self.client.force_login(self.user_b) + res = self.client.get( + reverse("api:pictures") + + f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}" + ) + assert res.status_code == 200 + expected = list( + self.user_a.pictures.union(self.user_b.pictures.all()) + .order_by("-picture__date") + .values_list("picture_id", flat=True) + ) + assert [i["id"] for i in res.json()] == expected + + def test_not_subscribed_user(self): + """Test that a user that is not subscribed can only its own pictures.""" + self.client.force_login(self.user_a) + res = self.client.get( + reverse("api:pictures") + f"?users_identified={self.user_a.id}" + ) + assert res.status_code == 200 + expected = list( + self.user_a.pictures.order_by("-picture__date").values_list( + "picture_id", flat=True + ) + ) + assert [i["id"] for i in res.json()] == expected + + # trying to access the pictures of someone else + res = self.client.get( + reverse("api:pictures") + f"?users_identified={self.user_b.id}" + ) + assert res.status_code == 403 + + # trying to access the pictures of someone else shouldn't success, + # even if mixed with owned pictures + res = self.client.get( + reverse("api:pictures") + + f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}" + ) + assert res.status_code == 403 diff --git a/sas/tests.py b/sas/tests/tests.py similarity index 100% rename from sas/tests.py rename to sas/tests/tests.py