diff --git a/amelie/graphql/tests/__init__.py b/amelie/graphql/tests/__init__.py index 0c1e98a..f732e41 100644 --- a/amelie/graphql/tests/__init__.py +++ b/amelie/graphql/tests/__init__.py @@ -86,7 +86,7 @@ def _test_private_model(self, query_name: str, public_field_spec: str = "id", ) def _test_private_model_list(self, query_name: str, public_field_spec: str = "id", - variables: Optional[Dict[str, Tuple[str, str]]] = None, + variables: Optional[Dict[str, Tuple[Union[str, int], str]]] = None, error_regex: Optional[re.Pattern] = None): """ Test if a model instance that should be private is not in the list returned by a GraphQL query. diff --git a/amelie/graphql/tests/test_activities.py b/amelie/graphql/tests/test_activities.py index a6c8c23..c4ec594 100644 --- a/amelie/graphql/tests/test_activities.py +++ b/amelie/graphql/tests/test_activities.py @@ -163,7 +163,7 @@ def test_activities_private_photo(self): response = self.query(query, variables={"id": self.public_activity.id}) content = json.loads(response.content) - # The request should succeedte + # The request should succeed self.assertResponseNoErrors( response, f"Query for 'activities', public field 'photos' returned an error!" diff --git a/amelie/graphql/tests/test_news.py b/amelie/graphql/tests/test_news.py new file mode 100644 index 0000000..73a4027 --- /dev/null +++ b/amelie/graphql/tests/test_news.py @@ -0,0 +1,203 @@ +import json + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils import timezone + +from amelie.activities.models import Activity +from amelie.news.models import NewsItem +from amelie.files.models import Attachment +from amelie.members.models import Committee, Person +from amelie.graphql.tests import BaseGraphQLPrivateFieldTests +from amelie.tools.tests import generate_activities + + +def generate_news_article() -> NewsItem: + """ + Generate News article for testing. + + It will generate 1 article with: + - a public attachment + - a private attachment + - a linked public activity + - a linked private activity + """ + + now = timezone.now() + committee = Committee.objects.first() + author = Person.objects.first() + + # Generate two activities, one public and one private + generate_activities(2) + + item = NewsItem( + publication_date=now, + title_nl=f"Nieuwsartikel", + title_en=f"News Article", + introduction_nl="Dit is een nieuwsartikel.", + introduction_en="This is a news article.", + content_nl="Dit is de inhoud.", + content_en="This is the content.", + publisher=committee, + author=author, + ) + item.save() + + # Add public attachment + public_attachment = Attachment( + public=True, file=SimpleUploadedFile("public.txt", b"File Contents") + ) + public_attachment.save(create_thumbnails=False) + item.attachments.add(public_attachment) + + # Add private attachment + private_attachment = Attachment( + public=False, file=SimpleUploadedFile("public.txt", b"File Contents") + ) + private_attachment.save(create_thumbnails=False) + item.attachments.add(private_attachment) + + # Add linked public activity + public_activity = Activity.objects.filter(public=True).order_by('-id').first() + item.activities.add(public_activity) + + # Add linked private activity + private_activity = Activity.objects.filter(public=False).order_by('-id').first() + item.activities.add(private_activity) + + return item + +class NewsGraphQLPrivateFieldTests(BaseGraphQLPrivateFieldTests): + """ + Tests for private fields of models of the News app + """ + + def setUp(self): + super(NewsGraphQLPrivateFieldTests, self).setUp() + + # Create required committees for the news module + educom = Committee(name="EduCom", abbreviation=settings.EDUCATION_COMMITTEE_ABBR) + educom.save() + + # Generate news article + self.article = generate_news_article() + + def test_news_item_private_attachment(self): + # Test if private attachments are hidden in get view + query = "query ($id: ID) { newsItem(id: $id) { attachments { public }}}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItem', public field 'attachments' returned an error!" + ) + + # Check that all attachments are public, and that the correct amount of attachments are received (1) + self.assertTrue(all(a['public'] == True for a in content['data']['newsItem']['attachments']), + f"Query for 'newsItem', public field 'attachments' returned a private attachment!") + num_attachments = len(content['data']['newsItem']['attachments']) + self.assertEqual( + num_attachments, 1, + f"Query for 'newsItem', public field 'attachments' did not return 1 expected attachment (returned {num_attachments})!" + ) + + def test_news_items_private_attachment(self): + # Test if private attachments are hidden in list view + query = "query ($id: ID) { newsItems(id: $id) { results { attachments { public }}}}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItems', public field 'attachments' returned an error!" + ) + + # Check that all attachments are public, and that the correct amount of attachments are received (1) + self.assertTrue(all(a['public'] == True for a in content['data']['newsItems']['results'][0]['attachments']), + f"Query for 'newsItems', public field 'attachments' returned a private attachment!") + num_attachments = len(content['data']['newsItems']['results'][0]['attachments']) + self.assertEqual( + num_attachments, 1, + f"Query for 'newsItems', public field 'attachments' did not return 1 expected attachment (returned {num_attachments})!" + ) + + def test_news_item_private_activity(self): + # Test if private activities are hidden in get view + query = "query ($id: ID) { newsItem(id: $id) { activities { public }}}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItem', public field 'activities' returned an error!" + ) + + # Check that all activities are public, and that the correct amount of activities are received (1) + self.assertTrue(all(a['public'] == True for a in content['data']['newsItem']['activities']), + f"Query for 'newsItem', public field 'activities' returned a private attachment!") + num_activities = len(content['data']['newsItem']['activities']) + self.assertEqual( + num_activities, 1, + f"Query for 'newsItem', public field 'activities' did not return 1 expected attachment (returned {num_activities})!" + ) + + def test_news_items_private_activity(self): + # Test if private activities are hidden in list view + query = "query ($id: ID) { newsItems(id: $id) { results { activities { public }}}}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItems', public field 'activities' returned an error!" + ) + + # Check that all activities are public, and that the correct amount of activities are received (1) + self.assertTrue(all(a['public'] == True for a in content['data']['newsItems']['results'][0]['activities']), + f"Query for 'newsItems', public field 'activities' returned a private attachment!") + num_activities = len(content['data']['newsItems']['results'][0]['activities']) + self.assertEqual( + num_activities, 1, + f"Query for 'newsItems', public field 'activities' did not return 1 expected activity (returned {num_activities})!" + ) + + def test_news_item_author_publisher_string(self): + # Test if the author and publisher of a news item are a strings in get view + query = "query ($id: ID) { newsItem(id: $id) { author, publisher }}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItem', public fields 'author, publisher' returned an error!" + ) + + # Check that both author and publisher fields are strings + self.assertTrue(isinstance(content['data']['newsItem']['author'], str), + f"Query for 'newsItem', public field 'author' returned something else than a string!") + self.assertTrue(isinstance(content['data']['newsItem']['publisher'], str), + f"Query for 'newsItem', public field 'author' returned something else than a string!") + + def test_news_items_author_publisher_string(self): + # Test if the author and publisher of a news item are a strings in list view + query = "query ($id: ID) { newsItems(id: $id) { results { author, publisher }}}" + response = self.query(query, variables={"id": self.article.id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'newsItems', public fields 'author, publisher' returned an error!" + ) + + # Check that both author and publisher fields are strings + self.assertTrue(isinstance(content['data']['newsItems']['results'][0]['author'], str), + f"Query for 'newsItem', public field 'author' returned something else than a string!") + self.assertTrue(isinstance(content['data']['newsItems']['results'][0]['publisher'], str), + f"Query for 'newsItem', public field 'author' returned something else than a string!") diff --git a/amelie/graphql/tests/test_publications.py b/amelie/graphql/tests/test_publications.py new file mode 100644 index 0000000..80b9c59 --- /dev/null +++ b/amelie/graphql/tests/test_publications.py @@ -0,0 +1,61 @@ +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils import timezone + +from amelie.graphql.tests import BaseGraphQLPrivateFieldTests +from amelie.publications.models import Publication, PublicationType + + +def generate_publication() -> Publication: + """ + Generate Publication for testing. + + It will generate 1 private publication + """ + + now = timezone.now() + + # Create PublicationType + publication_type = PublicationType( + type_name="Publication Type", + description="This is a publication type.", + default_thumbnail=SimpleUploadedFile("thumb.png", b"Some PNG") + ) + publication_type.save() + # Create publication + item = Publication( + name=f"Publication", + description="This is a publication", + date_published=now, + publication_type=publication_type, + file=SimpleUploadedFile("publication.txt", b"This is a very nice publication"), + public=False + ) + item.save() + return item + + +class PublicationsGraphQLPrivateFieldTests(BaseGraphQLPrivateFieldTests): + """ + Tests for private fields of models of the Publications app + """ + + def setUp(self): + super(PublicationsGraphQLPrivateFieldTests, self).setUp() + + # Generate two publications, one public and one private + self.private_publication = generate_publication() + + def test_publication_private_model(self): + # Test if private publication cannot be retrieved + self._test_private_model( + query_name="publication", + variables={"id": (self.private_publication.id, "ID")} + ) + + def test_publications_private_model(self): + # Test if private publication cannot be retrieved via list view + self._test_private_model_list( + query_name="publications", + public_field_spec="results { id }", + variables={"id": (self.private_publication.id, "ID")} + ) diff --git a/amelie/graphql/tests/test_videos.py b/amelie/graphql/tests/test_videos.py new file mode 100644 index 0000000..14d727b --- /dev/null +++ b/amelie/graphql/tests/test_videos.py @@ -0,0 +1,127 @@ +import json +from typing import Tuple + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils import timezone + +from amelie.activities.models import Activity +from amelie.news.models import NewsItem +from amelie.files.models import Attachment +from amelie.members.models import Committee, Person +from amelie.graphql.tests import BaseGraphQLPrivateFieldTests +from amelie.videos.models import BaseVideo, YouTubeVideo, StreamingIAVideo +from amelie.tools.tests import generate_activities + + +def generate_videos(): + """ + Generate Video for testing. + + It will generate 4 videos + - A public YouTube video + - A public Streaming.IA video + - A private YouTube video + - A private Streaming.IA video + """ + + now = timezone.now() + committee = Committee.objects.first() + + for i in range(2): + # Create video + item = YouTubeVideo( + video_id=i, + title=f"YouTube Video {i + 1}", + description="This is a Youtube video.", + date_published=now, + publisher=committee, + public=bool(i) + ) + item.save() + item = StreamingIAVideo( + video_id=i, + title=f"Streaming.IA Video {i + 1}", + description="This is a Streaming.IA video.", + date_published=now, + publisher=committee, + public=bool(i) + ) + item.save() + + +class VideosGraphQLPrivateFieldTests(BaseGraphQLPrivateFieldTests): + """ + Tests for private fields of models of the Videos app + """ + + def setUp(self): + super(VideosGraphQLPrivateFieldTests, self).setUp() + + # Generate four videos, public and private, YouTube and Streaming.IA videos. + generate_videos() + + # Retrieve and store those videos + self.public_yt_video = YouTubeVideo.objects.filter(public=True).order_by('-video_id').first() + self.private_yt_video = YouTubeVideo.objects.filter(public=False).order_by('-video_id').first() + self.public_ia_video = StreamingIAVideo.objects.filter(public=True).order_by('-video_id').first() + self.private_ia_video = StreamingIAVideo.objects.filter(public=False).order_by('-video_id').first() + + def test_video_private_model(self): + # Test if private videos cannot be retrieved + self._test_private_model( + query_name="video", + public_field_spec="videoId", + variables={"videoId": (self.private_yt_video.video_id, "ID")} + ) + self._test_private_model( + query_name="video", + public_field_spec="videoId", + variables={"videoId": (self.private_ia_video.video_id, "ID")} + ) + + def test_videos_private_model(self): + # Test if private videos cannot be retrieved via list view + self._test_private_model_list( + query_name="videos", + public_field_spec="results { videoId }", + variables={"videoId": (self.private_yt_video.video_id, "ID")} + ) + self._test_private_model_list( + query_name="videos", + public_field_spec="results { videoId }", + variables={"videoId": (self.private_ia_video.video_id, "ID")} + ) + + def test_video_publisher_string(self): + # Test if the publisher of a video is a string in get view + query = "query ($videoId: ID) { video(videoId: $videoId) { publisher }}" + response = self.query(query, variables={"videoId": self.public_yt_video.video_id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'video', public field 'publisher' returned an error!" + ) + + # Check that both author and publisher fields are strings + self.assertTrue(isinstance(content['data']['video']['publisher'], str), + f"Query for 'video', public field 'publisher' returned something else than a string!") + + def test_videos_publisher_string(self): + # Test if the publisher of a video is a string in list view + query = "query ($videoId: ID) { videos(videoId: $videoId) { results { publisher }}}" + response = self.query(query, variables={"videoId": self.public_yt_video.video_id}) + content = json.loads(response.content) + + # The request should succeed + self.assertResponseNoErrors( + response, + f"Query for 'videos', public field 'publisher' returned an error!" + ) + + # Check that both author and publisher fields are strings + self.assertTrue(isinstance(content['data']['videos']['results'][0]['publisher'], str), + f"Query for 'newsItem', public field 'publisher' returned something else than a string!") + diff --git a/amelie/news/graphql.py b/amelie/news/graphql.py index 0a25e6f..15becd6 100644 --- a/amelie/news/graphql.py +++ b/amelie/news/graphql.py @@ -12,7 +12,6 @@ class Meta: model = NewsItem description = "Type definition for a single News Item" filter_fields = { - 'id': ("exact", ), 'title_nl': ("icontains", "iexact"), 'title_en': ("icontains", "iexact"), 'publication_date': ("exact", "gt", "lt"), @@ -47,6 +46,10 @@ def resolve_attachments(self: NewsItem, info): # `info.context` is the Django Request object in Graphene return self.attachments.filter_public(info.context).all() + def resolve_activities(self: NewsItem, info): + # `info.context` is the Django Request object in Graphene + return self.activities.filter_public(info.context).all() + def resolve_author(obj: NewsItem, info): return obj.author.incomplete_name() @@ -65,11 +68,19 @@ def resolve_content(obj: NewsItem, info): class NewsQuery(graphene.ObjectType): news_item = graphene.Field(NewsItemType, id=graphene.ID()) - news_items = DjangoPaginationConnectionField(NewsItemType) + news_items = DjangoPaginationConnectionField(NewsItemType, id=graphene.ID()) def resolve_news_item(root, info, id): return NewsItem.objects.get(pk=id) + def resolve_news_items(root, info, id=None, *args, **kwargs): + """Find news items by ID""" + qs = NewsItem.objects + # Find the news item by its ID + if id is not None: + return qs.filter(pk=id) + return qs + # Exports GRAPHQL_QUERIES = [NewsQuery] diff --git a/amelie/publications/graphql.py b/amelie/publications/graphql.py index e8ab13f..91af8b6 100644 --- a/amelie/publications/graphql.py +++ b/amelie/publications/graphql.py @@ -1,3 +1,5 @@ +from datetime import time + import graphene from graphene_django import DjangoObjectType @@ -21,7 +23,6 @@ class Meta: model = Publication description = "Type definition for a single Publication" filter_fields = { - 'id': ("exact",), 'name': ("icontains", "iexact"), 'date_published': ("exact", "gt", "lt"), 'publication_type__type_name': ("icontains", "iexact"), @@ -39,21 +40,25 @@ class Meta: "public", ] - @classmethod - def get_queryset(cls, queryset, info): - return queryset.filter_public(info.context) - def resolve_thumbnail(obj: Publication, info): return obj.get_thumbnail() class PublicationQuery(graphene.ObjectType): publication = graphene.Field(PublicationItemType, id=graphene.ID()) - publications = DjangoPaginationConnectionField(PublicationItemType) + publications = DjangoPaginationConnectionField(PublicationItemType, id=graphene.ID()) def resolve_publication(root, info, id): return Publication.objects.filter_public(info.context).get(pk=id) + def resolve_publications(root, info, id=None, *args, **kwargs): + """Find publications by ID""" + qs = Publication.objects.filter_public(info.context) + # Find the publication by its ID + if id is not None: + return qs.filter(pk=id) + return qs + # Exports GRAPHQL_QUERIES = [PublicationQuery] diff --git a/amelie/videos/graphql.py b/amelie/videos/graphql.py index c34c719..f786b75 100644 --- a/amelie/videos/graphql.py +++ b/amelie/videos/graphql.py @@ -13,7 +13,6 @@ class VideoFilterSet(django_filters.FilterSet): class Meta: model = BaseVideo fields = { - 'video_id': ("exact",), 'title': ("icontains", "iexact"), 'date_published': ("exact", "gt", "lt"), 'publisher__name': ("icontains", "iexact"), @@ -55,10 +54,6 @@ class Meta: video_type = graphene.String(description=_("Video type (Youtube or IA)")) video_url = graphene.String(description=_("URL to the video")) - @classmethod - def get_queryset(cls, queryset, info): - return queryset.filter_public(info.context) - def resolve_publisher(obj: BaseVideo, info): return obj.publisher.name @@ -70,11 +65,19 @@ def resolve_video_url(obj: BaseVideo, info): class VideoQuery(graphene.ObjectType): - video = graphene.Field(VideoType, id=graphene.ID()) - videos = DjangoPaginationConnectionField(VideoType) - - def resolve_video(root, info, id): - return BaseVideo.objects.filter_public(info.context).get(pk=id) + video = graphene.Field(VideoType, video_id=graphene.ID()) + videos = DjangoPaginationConnectionField(VideoType, video_id=graphene.ID()) + + def resolve_video(root, info, video_id): + return BaseVideo.objects.filter_public(info.context).get(video_id=video_id) + + def resolve_videos(root, info, video_id=None, *args, **kwargs): + """Find videos by ID""" + qs = BaseVideo.objects.filter_public(info.context) + # Find the video by its Video ID + if video_id is not None: + return qs.filter(video_id=video_id) + return qs # Exports