diff --git a/.coverage b/.coverage index a544a4fb6..9be488fc3 100644 Binary files a/.coverage and b/.coverage differ diff --git a/.gitignore b/.gitignore index 8aac9a272..259751217 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ uploads /data/ cypress/screenshots /package-lock.json +/frontend/package-lock.json +/frontend/frontend/package-lock.json +package-lock.json diff --git a/api/middleware.py b/api/middleware.py index 9bb4e33ca..5c1cd2074 100644 --- a/api/middleware.py +++ b/api/middleware.py @@ -85,7 +85,6 @@ def __call__(self, request): class DisableCSRFMiddleware(object): - def __init__(self, get_response): self.get_response = get_response diff --git a/api/models/indiening.py b/api/models/indiening.py index 9abd6d821..5190e4f2b 100644 --- a/api/models/indiening.py +++ b/api/models/indiening.py @@ -72,10 +72,6 @@ def __str__(self): return str(self.indiening_id) def save(self, *args, **kwargs): - # First save to generate the indiening_id if it doesn't exist - if not self.indiening_id: - super(Indiening, self).save(*args, **kwargs) - # Update the bestand path if it's still using the temporary path if "temp" in self.bestand.name: old_file = self.bestand diff --git a/api/models/project.py b/api/models/project.py index 3e72e193b..6cf3b282b 100644 --- a/api/models/project.py +++ b/api/models/project.py @@ -55,8 +55,8 @@ class Project(models.Model): vak = models.ForeignKey(Vak, on_delete=models.CASCADE) deadline = models.DateTimeField(null=True, blank=True) extra_deadline = models.DateTimeField(null=True, blank=True) - max_score = models.IntegerField(default=20) - max_groep_grootte = models.IntegerField(default=1) + max_score = models.PositiveSmallIntegerField(default=20) + max_groep_grootte = models.PositiveSmallIntegerField(default=1) student_groep = models.BooleanField(default=False, blank=True) zichtbaar = models.BooleanField(default=True, blank=True) gearchiveerd = models.BooleanField(default=False, blank=True) diff --git a/api/models/score.py b/api/models/score.py index f94565b91..c296e02fc 100644 --- a/api/models/score.py +++ b/api/models/score.py @@ -19,7 +19,7 @@ class Score(models.Model): """ score_id = models.AutoField(primary_key=True) - score = models.SmallIntegerField() + score = models.PositiveSmallIntegerField() indiening = models.ForeignKey("Indiening", on_delete=models.CASCADE) def __str__(self): diff --git a/api/models/vak.py b/api/models/vak.py index fd78219c8..48f0124bb 100644 --- a/api/models/vak.py +++ b/api/models/vak.py @@ -22,7 +22,7 @@ class Vak(models.Model): vak_id = models.AutoField(primary_key=True) naam = models.CharField(max_length=100) - jaartal = models.IntegerField(default=date.today().year) + jaartal = models.PositiveSmallIntegerField(default=date.today().year) gearchiveerd = models.BooleanField(default=False, blank=True) studenten = models.ManyToManyField( "Gebruiker", related_name="vak_gebruikers", blank=True diff --git a/api/tests/factories/indiening.py b/api/tests/factories/indiening.py index 35563f489..879995690 100644 --- a/api/tests/factories/indiening.py +++ b/api/tests/factories/indiening.py @@ -1,5 +1,5 @@ import factory -from api.models.indiening import Indiening, IndieningBestand +from api.models.indiening import Indiening from factory.django import DjangoModelFactory from factory import SubFactory from .groep import GroepFactory @@ -23,16 +23,5 @@ class Meta: ) status = factory.Faker("boolean") result = factory.Faker("paragraph") - artefacten = None - - indiening_bestanden = factory.RelatedFactory( - "api.tests.factories.indiening.IndieningBestandFactory", "indiening" - ) - - -class IndieningBestandFactory(DjangoModelFactory): - class Meta: - model = IndieningBestand - - indiening = SubFactory(IndieningFactory) bestand = FileField(filename="test.txt", data=b"file content") + artefacten = None diff --git a/api/tests/factories/project.py b/api/tests/factories/project.py index ec5e56923..f561c8d8d 100644 --- a/api/tests/factories/project.py +++ b/api/tests/factories/project.py @@ -29,5 +29,6 @@ class Meta: ) max_score = factory.Faker("random_int", min=10, max=100) max_groep_grootte = factory.Faker("random_int", min=1, max=5) + student_groep = factory.Faker("boolean") zichtbaar = factory.Faker("boolean") gearchiveerd = factory.Faker("boolean") diff --git a/api/tests/factories/score.py b/api/tests/factories/score.py index 844bf18d4..97ff855fb 100644 --- a/api/tests/factories/score.py +++ b/api/tests/factories/score.py @@ -14,6 +14,5 @@ class Meta: @classmethod def _create(cls, model_class, *args, **kwargs): indiening = kwargs.pop("indiening") - max_score = indiening.groep.project.max_score - kwargs["score"] = random.randint(0, max_score) + kwargs["score"] = random.randint(0, indiening.groep.project.max_score) return super()._create(model_class, indiening=indiening, *args, **kwargs) diff --git a/api/tests/factories/template.py b/api/tests/factories/template.py new file mode 100644 index 000000000..1b52c7f8b --- /dev/null +++ b/api/tests/factories/template.py @@ -0,0 +1,12 @@ +import factory +from factory.django import FileField +from api.models.template import Template +from api.tests.factories.gebruiker import UserFactory + + +class TemplateFactory(factory.django.DjangoModelFactory): + class Meta: + model = Template + + user = factory.SubFactory(UserFactory) + bestand = FileField(filename="template.txt", data=b"file content") diff --git a/api/tests/models/test_indiening.py b/api/tests/models/test_indiening.py index e0f065d06..cc579b813 100644 --- a/api/tests/models/test_indiening.py +++ b/api/tests/models/test_indiening.py @@ -1,11 +1,16 @@ from django.test import TestCase -from django.core.files.uploadedfile import SimpleUploadedFile -from api.tests.factories.indiening import IndieningFactory, IndieningBestandFactory +from api.tests.factories.indiening import IndieningFactory from api.models.indiening import upload_to +from unittest.mock import patch, MagicMock, call +from api.models.indiening import send_indiening_confirmation_mail +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import os class IndieningModelTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.indiening = IndieningFactory.create() def test_str_method(self): @@ -20,9 +25,6 @@ def test_tijdstip(self): def test_status(self): self.assertIsNotNone(self.indiening.status) - def test_indiening_bestanden(self): - self.assertEqual(self.indiening.indiening_bestanden.count(), 1) - def test_upload_to(self): filename = "test_indiening.txt" expected_path = ( @@ -30,20 +32,116 @@ def test_upload_to(self): ) self.assertEqual(upload_to(self.indiening, filename), expected_path) + def test_artefacten(self): + self.assertIsNotNone(self.indiening.artefacten) + + def test_result(self): + self.assertIsNotNone(self.indiening.result) + + def test_bestand(self): + self.assertIsNotNone(self.indiening.bestand) + + @patch("smtplib.SMTP_SSL") + @patch("ssl.create_default_context") + def test_send_indiening_confirmation_mail( + self, mock_create_default_context, mock_smtp + ): + indiening = self.indiening + project = indiening.groep.project + + smtp_server_address = "smtp.gmail.com" + smtp_port = 465 + mail_username = os.environ.get("MAIL_USERNAME") + mail_app_password = os.environ.get("MAIL_APP_PASSWORD") + indiening_status = { + -1: "heeft niet alle testen geslaagd.", + 0: "wordt nog getest...", + 1: "heeft alle testen geslaagd!", + } + project_url = f"https://sel2-4.ugent.be/course/{project.vak.vak_id}/assignment/{project.project_id}" + indiening_url = f"https://sel2-4.ugent.be/course/{project.vak.vak_id}/assignment/ \ + {project.project_id}/submission/{indiening.indiening_id}" + + # Setup the mock SMTP_SSL object + mock_smtp_instance = MagicMock() + mock_smtp.return_value.__enter__.return_value = mock_smtp_instance + + # Call the function + send_indiening_confirmation_mail(indiening) + + # Check that create_default_context was called once + assert mock_create_default_context.call_count == 1 -class IndieningBestandModelTest(TestCase): - def setUp(self): - self.indiening_bestand = IndieningBestandFactory.create( - bestand=SimpleUploadedFile("file.txt", b"file_content") + # Check that SMTP_SSL was called with the correct arguments + assert mock_smtp.call_count == indiening.groep.studenten.count() + mock_smtp.assert_has_calls( + [ + call( + smtp_server_address, + smtp_port, + context=mock_create_default_context.return_value, + ) + ] + * indiening.groep.studenten.count() ) - def test_str_method(self): - self.assertEqual( - str(self.indiening_bestand), str(self.indiening_bestand.bestand.name) + # Check that login was called with the correct arguments + mock_smtp_instance.login.assert_called_with(mail_username, mail_app_password) + + # Check that sendmail was called once for each student + assert ( + mock_smtp_instance.sendmail.call_count == indiening.groep.studenten.count() ) - def test_indiening(self): - self.assertIsNotNone(self.indiening_bestand.indiening) + # Check that sendmail was called with the correct arguments + for student in indiening.groep.studenten.all(): + subject = "Indieningsontvangst" - def test_bestand(self): - self.assertIsNotNone(self.indiening_bestand.bestand) + email = MIMEMultipart("alternative") + email["Subject"] = subject + email["From"] = mail_username + email["To"] = student.user.email + + plain_text = f""" + Beste {student.user.first_name} {student.user.last_name}, + + Dit is een bevestiging dat uw indiening voor het project {project.titel} is ontvangen. + De indiening {indiening_status[indiening.status]}. + """ + + html_text = f""" + + +

Beste {student.user.first_name} {student.user.last_name}

+

Dit is een bevestiging dat uw indiening voor het project \ + {project.titel} is ontvangen.

+

De indiening {indiening_status[indiening.status]}

+ + + """ + + email.attach(MIMEText(plain_text, "plain")) + email.attach(MIMEText(html_text, "html")) + + call_args_list = mock_smtp_instance.sendmail.call_args_list + for mock_call in call_args_list: + ( + sent_mail_username, + sent_student_email, + sent_email_content, + ) = mock_call.args + assert sent_mail_username == mail_username + assert sent_student_email == student.user.email + assert subject in sent_email_content + assert ( + f"Beste {student.user.first_name} {student.user.last_name}" + in sent_email_content + ) + assert ( + f"Dit is een bevestiging dat uw indiening voor het project {project.titel} is ontvangen." + in sent_email_content + ) + assert ( + f"De indiening {indiening_status[indiening.status]}" + in sent_email_content + ) diff --git a/api/tests/models/test_score.py b/api/tests/models/test_score.py index 63b46f676..c6046e72b 100644 --- a/api/tests/models/test_score.py +++ b/api/tests/models/test_score.py @@ -1,10 +1,12 @@ from django.test import TestCase from api.tests.factories.score import ScoreFactory from api.tests.factories.indiening import IndieningFactory +from unittest.mock import patch class ScoreModelTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.indiening = IndieningFactory.create() self.score = ScoreFactory.create(indiening=self.indiening) diff --git a/api/tests/models/test_template.py b/api/tests/models/test_template.py new file mode 100644 index 000000000..8c95e4816 --- /dev/null +++ b/api/tests/models/test_template.py @@ -0,0 +1,21 @@ +from django.test import TestCase +from api.tests.factories.template import TemplateFactory +from api.models.template import Template +from api.models.template import upload_to + + +class TemplateModelTest(TestCase): + def setUp(self): + self.template = TemplateFactory.create() + + def test_template_creation(self): + self.assertIsInstance(self.template, Template) + self.assertEqual(Template.objects.count(), 1) + + def test_template_str(self): + self.assertEqual(str(self.template), self.template.bestand.name) + + def test_upload_to(self): + filename = "test_template.txt" + expected_path = f"data/templates/gebruiker_{self.template.user.id}/{filename}" + self.assertEqual(upload_to(self.template, filename), expected_path) diff --git a/api/tests/serializers/test_indiening.py b/api/tests/serializers/test_indiening.py index d0e9b53d0..24821279c 100644 --- a/api/tests/serializers/test_indiening.py +++ b/api/tests/serializers/test_indiening.py @@ -1,18 +1,16 @@ from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile -from api.serializers.indiening import IndieningSerializer, IndieningBestandSerializer -from api.tests.factories.indiening import IndieningFactory, IndieningBestandFactory +from api.serializers.indiening import IndieningSerializer +from api.tests.factories.indiening import IndieningFactory from api.tests.factories.groep import GroepFactory -from api.tests.factories.restrictie import RestrictieFactory +from unittest.mock import patch class IndieningSerializerTest(TestCase): - def setUp(self): - self.indiening = IndieningFactory.create() - self.serializer = IndieningSerializer(instance=self.indiening) - self.restrictie = RestrictieFactory(project=self.indiening.groep.project) - - def test_indiening_serializer_fields(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_indiening_serializer_fields(self, mock_send_mail): + indiening = IndieningFactory.create() + self.serializer = IndieningSerializer(instance=indiening) data = self.serializer.data self.assertEqual( set(data.keys()), @@ -24,61 +22,42 @@ def test_indiening_serializer_fields(self): "status", "result", "artefacten", - "indiening_bestanden", + "bestand", ] ), ) - def test_indiening_serializer_create(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_indiening_serializer_create(self, mock_send_mail): groep = GroepFactory.create() - data = {"groep": groep.groep_id} + data = { + "groep": groep.groep_id, + "bestand": SimpleUploadedFile("bestand.txt", b"bestand_content"), + } serializer = IndieningSerializer(data=data) self.assertTrue(serializer.is_valid()) indiening = serializer.save() self.assertEqual(indiening.groep, groep) - def test_indiening_serializer_update(self): - new_data = {"groep": self.indiening.groep.groep_id} - serializer = IndieningSerializer( - instance=self.indiening, data=new_data, partial=True - ) - self.assertTrue(serializer.is_valid()) - indiening = serializer.save() - self.assertEqual(indiening.groep, self.indiening.groep) - - -class IndieningBestandSerializerTest(TestCase): - def setUp(self): - self.indiening_bestand = IndieningBestandFactory.create( - bestand=SimpleUploadedFile("file.txt", b"file_content") - ) - self.serializer = IndieningBestandSerializer(instance=self.indiening_bestand) + def test_indiening_serializer_no_file(self): + groep = GroepFactory.create() + data = {"groep": groep.groep_id} + serializer = IndieningSerializer(data=data) + self.assertFalse(serializer.is_valid()) - def test_indiening_bestand_serializer_fields(self): - data = self.serializer.data - self.assertEqual( - set(data.keys()), set(["indiening_bestand_id", "indiening", "bestand"]) - ) + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_indiening_serializer_no_groep(self, mock_send_mail): + data = {"bestand": SimpleUploadedFile("bestand.txt", b"bestand_content")} + serializer = IndieningSerializer(data=data) + self.assertFalse(serializer.is_valid()) - def test_indiening_bestand_serializer_create(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_indiening_serializer_update(self, mock_send_mail): indiening = IndieningFactory.create() - data = { - "indiening": indiening.indiening_id, - "bestand": SimpleUploadedFile("file.txt", b"file_content"), - } - serializer = IndieningBestandSerializer(data=data) - self.assertTrue(serializer.is_valid()) - indiening_bestand = serializer.save() - self.assertEqual(indiening_bestand.indiening, indiening) - - def test_indiening_bestand_serializer_update(self): - new_data = { - "indiening": self.indiening_bestand.indiening.indiening_id, - "bestand": SimpleUploadedFile("file.txt", b"file_content"), - } - serializer = IndieningBestandSerializer( - instance=self.indiening_bestand, data=new_data, partial=True + new_data = {"groep": indiening.groep.groep_id} + serializer = IndieningSerializer( + instance=indiening, data=new_data, partial=True ) self.assertTrue(serializer.is_valid()) - indiening_bestand = serializer.save() - self.assertEqual(indiening_bestand.indiening, self.indiening_bestand.indiening) + indiening = serializer.save() + self.assertEqual(indiening.groep, indiening.groep) diff --git a/api/tests/serializers/test_project.py b/api/tests/serializers/test_project.py index c5488b605..d49b84607 100644 --- a/api/tests/serializers/test_project.py +++ b/api/tests/serializers/test_project.py @@ -6,6 +6,7 @@ from api.tests.factories.vak import VakFactory from django.core.files.uploadedfile import SimpleUploadedFile from datetime import datetime, timedelta +from django.utils import timezone class ProjectSerializerTest(APITestCase): @@ -87,6 +88,7 @@ def test_validation_for_blank_items(self): "extra_deadline": "", "max_score": "", "max_groep_grootte": "", + "student_groep": "", "zichtbaar": "", "gearchiveerd": "", } @@ -100,17 +102,38 @@ def test_create(self): "beschrijving": "Dit is een test project.", "opgave_bestand": SimpleUploadedFile("file.txt", b"file_content"), "vak": vak, - "deadline": self.serializer.data["deadline"], - "extra_deadline": self.serializer.data["extra_deadline"], + "deadline": timezone.now() + timedelta(days=1), + "extra_deadline": timezone.now() + timedelta(days=2), "max_score": 20, "max_groep_grootte": 1, + "student_groep": False, "zichtbaar": True, "gearchiveerd": False, } serializer = ProjectSerializer(data=data) self.assertTrue(serializer.is_valid()) project = serializer.save() - self.assertEqual(project.deadline, parse(data["deadline"])) + self.assertEqual(project.deadline, data["deadline"]) + + def test_create_higher_grootte(self): + vak = VakFactory.create().vak_id + data = { + "titel": "test project", + "beschrijving": "Dit is een test project.", + "opgave_bestand": SimpleUploadedFile("file.txt", b"file_content"), + "vak": vak, + "deadline": timezone.now() + timedelta(days=1), + "extra_deadline": timezone.now() + timedelta(days=2), + "max_score": 20, + "max_groep_grootte": 5, + "student_groep": False, + "zichtbaar": True, + "gearchiveerd": False, + } + serializer = ProjectSerializer(data=data) + self.assertTrue(serializer.is_valid()) + project = serializer.save() + self.assertEqual(project.deadline, data["deadline"]) def test_create_no_deadline(self): vak = VakFactory.create().vak_id @@ -123,6 +146,7 @@ def test_create_no_deadline(self): "extra_deadline": None, "max_score": 20, "max_groep_grootte": 1, + "student_groep": False, "zichtbaar": True, "gearchiveerd": False, } @@ -142,6 +166,7 @@ def test_create_invalid_deadline(self): "extra_deadline": self.serializer.data["extra_deadline"], "max_score": 20, "max_groep_grootte": 1, + "student_groep": False, "zichtbaar": True, "gearchiveerd": False, } @@ -160,6 +185,7 @@ def test_create_invalid_extra_deadline(self): "extra_deadline": datetime.now() - timedelta(days=1), "max_score": 20, "max_groep_grootte": 1, + "student_groep": False, "zichtbaar": True, "gearchiveerd": False, } @@ -177,6 +203,7 @@ def test_update(self): "extra_deadline": self.serializer.data["extra_deadline"], "max_score": 20, "max_groep_grootte": 1, + "student_groep": False, "zichtbaar": True, "gearchiveerd": False, } diff --git a/api/tests/serializers/test_score.py b/api/tests/serializers/test_score.py index 4e03e7892..decbb4494 100644 --- a/api/tests/serializers/test_score.py +++ b/api/tests/serializers/test_score.py @@ -3,10 +3,12 @@ from api.serializers.score import ScoreSerializer from api.tests.factories.score import ScoreFactory from api.tests.factories.indiening import IndieningFactory +from unittest.mock import patch class ScoreSerializerTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.score = ScoreFactory.create() self.score.score = self.score.indiening.groep.project.max_score self.score.save() @@ -28,7 +30,8 @@ def test_score_field_content(self): data = self.serializer.data self.assertEqual(data["score"], self.score.score) - def test_score_serializer_create(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_serializer_create(self, mock_send_mail): indiening = IndieningFactory.create() max_score = indiening.groep.project.max_score data = {"indiening": indiening.indiening_id, "score": max_score} @@ -38,7 +41,8 @@ def test_score_serializer_create(self): self.assertEqual(score.indiening, indiening) self.assertEqual(score.score, data["score"]) - def test_score_serializer_create_invalid(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_serializer_create_invalid(self, mock_send_mail): indiening = IndieningFactory.create() max_score = indiening.groep.project.max_score data = {"indiening": indiening.indiening_id, "score": max_score} @@ -50,7 +54,8 @@ def test_score_serializer_create_invalid(self): self.assertTrue(new_serializer.is_valid()) self.assertRaises(ValidationError, new_serializer.save, raise_exception=True) - def test_score_serializer_create_invalid_high_score(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_serializer_create_invalid_high_score(self, mock_send_mail): indiening = IndieningFactory.create() max_score = indiening.groep.project.max_score data = {"indiening": indiening.indiening_id, "score": max_score + 1} @@ -70,7 +75,8 @@ def test_score_serializer_update(self): score = serializer.save() self.assertEqual(score.score, new_data["score"]) - def test_score_serializer_update_invalid(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_serializer_update_invalid(self, mock_send_mail): indiening = IndieningFactory.create() new_data = { "score": indiening.groep.project.max_score, diff --git a/api/tests/serializers/test_template.py b/api/tests/serializers/test_template.py new file mode 100644 index 000000000..24747198d --- /dev/null +++ b/api/tests/serializers/test_template.py @@ -0,0 +1,35 @@ +from django.test import TestCase +from rest_framework.exceptions import ValidationError +from api.tests.factories.template import TemplateFactory +from api.serializers.template import TemplateSerializer + + +class TemplateSerializerTest(TestCase): + def setUp(self): + self.template = TemplateFactory.create() + self.serializer = TemplateSerializer(instance=self.template) + + def test_contains_expected_fields(self): + data = self.serializer.data + self.assertCountEqual(data.keys(), ["template_id", "user", "bestand"]) + + def test_template_id_field_content(self): + data = self.serializer.data + self.assertEqual(data["template_id"], self.template.template_id) + + def test_user_field_content(self): + data = self.serializer.data + self.assertEqual(data["user"], self.template.user.id) + + def test_bestand_field_content(self): + data = self.serializer.data + self.assertEqual(data["bestand"], self.template.bestand.url) + + def test_validation(self): + invalid_data = { + "user": "", + "bestand": "", + } + serializer = TemplateSerializer(data=invalid_data) + with self.assertRaises(ValidationError): + serializer.is_valid(raise_exception=True) diff --git a/api/tests/serializers/test_vak.py b/api/tests/serializers/test_vak.py index 513371c9a..9f7841453 100644 --- a/api/tests/serializers/test_vak.py +++ b/api/tests/serializers/test_vak.py @@ -181,3 +181,26 @@ def test_add_invalid_students_to_group(self): serializer = VakSerializer(instance=self.vak_data, data=data, partial=True) self.assertTrue(serializer.is_valid()) vak = serializer.save() + + def test_add_students_to_group_with_max_groep_grootte(self): + students_data = [ + GebruikerFactory.create(is_lesgever=False).user.id for _ in range(3) + ] + teachers_data = [ + GebruikerFactory.create(is_lesgever=True).user.id for _ in range(3) + ] + data = { + "naam": "test vak", + "studenten": students_data, + "lesgevers": teachers_data, + } + serializer = VakSerializer(instance=self.vak_data, data=data, partial=True) + self.assertTrue(serializer.is_valid()) + vak = serializer.save() + project = ProjectFactory.create( + student_groep=False, max_groep_grootte=2, vak=vak + ) + serializer = VakSerializer(instance=self.vak_data, data=data, partial=True) + self.assertTrue(serializer.is_valid()) + vak = serializer.save() + self.assertEqual(project.vak, vak) diff --git a/api/tests/views/test_indiening.py b/api/tests/views/test_indiening.py index 4ee807635..7e299b776 100644 --- a/api/tests/views/test_indiening.py +++ b/api/tests/views/test_indiening.py @@ -6,10 +6,12 @@ from rest_framework import status from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.base import ContentFile +from unittest.mock import patch class IndieningListViewTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.teacher = GebruikerFactory.create(is_lesgever=True) self.student = GebruikerFactory.create(is_lesgever=False) @@ -30,13 +32,13 @@ def test_indiening_list_get_as_student(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) - def test_indiening_list_post(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_indiening_list_post(self, mock_send_mail): data = { "groep": self.indiening2.groep_id, } - file1 = SimpleUploadedFile("file1.txt", b"file_content") - file2 = SimpleUploadedFile("file2.txt", b"file_content") - data["indiening_bestanden"] = [file1, file2] + file = SimpleUploadedFile("file1.txt", b"file_content") + data["bestand"] = file response = self.client.post(self.url, data, format="multipart") self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.get(self.url) @@ -97,7 +99,8 @@ def test_indiening_list_post_invalid(self): class IndieningDetailViewTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.gebruiker = GebruikerFactory.create() self.gebruiker.user.is_superuser = True @@ -134,7 +137,8 @@ def test_indiening_detail_delete_unauthorized(self): class IndieningDetailDownloadBestandenTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.teacher = GebruikerFactory.create(is_lesgever=True) self.student = GebruikerFactory.create(is_lesgever=False) @@ -168,7 +172,8 @@ def test_indiening_detail_download_bestanden_get_invalid(self): class IndieningDetailDownloadArtefactenTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.teacher = GebruikerFactory.create(is_lesgever=True) self.student = GebruikerFactory.create(is_lesgever=False) diff --git a/api/tests/views/test_project.py b/api/tests/views/test_project.py index 0ac75969d..0bc4d3a04 100644 --- a/api/tests/views/test_project.py +++ b/api/tests/views/test_project.py @@ -120,7 +120,7 @@ def test_project_detail_get_unauthorized(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_invalid_project_detail_get(self): - response = self.client.get(reverse("project_detail", kwargs={"id": 69})) + response = self.client.get(reverse("project_detail", kwargs={"id": 6969})) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_project_detail_put(self): diff --git a/api/tests/views/test_score.py b/api/tests/views/test_score.py index 8c3fd9337..961e49f5d 100644 --- a/api/tests/views/test_score.py +++ b/api/tests/views/test_score.py @@ -5,10 +5,12 @@ from rest_framework.test import APIClient from django.urls import reverse from rest_framework import status +from unittest.mock import patch class ScoreListViewTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.teacher = GebruikerFactory.create(is_lesgever=True) self.student = GebruikerFactory.create(is_lesgever=False) @@ -39,7 +41,8 @@ def test_score_list_get_invalid_indiening(self): response = self.client.get(self.url, {"indiening": "indiening"}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_score_list_post(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_list_post(self, mock_send_mail): indiening = IndieningFactory.create() data = { "score_id": self.score1.score_id, @@ -49,7 +52,8 @@ def test_score_list_post(self): response = self.client.post(self.url, data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - def test_score_list_post_unauthorized(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def test_score_list_post_unauthorized(self, mock_send_mail): self.client.force_login(self.student.user) indiening = IndieningFactory.create() data = { @@ -71,7 +75,8 @@ def test_score_list_post_invalid(self): class ScoreDetailViewTest(TestCase): - def setUp(self): + @patch("api.models.indiening.send_indiening_confirmation_mail") + def setUp(self, mock_send_mail): self.client = APIClient() self.teacher = GebruikerFactory.create(is_lesgever=True) self.student = GebruikerFactory.create(is_lesgever=False) diff --git a/api/tests/views/test_template.py b/api/tests/views/test_template.py new file mode 100644 index 000000000..e011a3379 --- /dev/null +++ b/api/tests/views/test_template.py @@ -0,0 +1,186 @@ +from django.urls import reverse +from rest_framework.test import APIClient, APITestCase +from api.tests.factories.template import TemplateFactory +from api.models.template import Template +from api.tests.factories.gebruiker import GebruikerFactory +from django.core.files.uploadedfile import SimpleUploadedFile +from rest_framework import status +from django.http import FileResponse + + +class TemplateListViewTest(APITestCase): + def setUp(self): + self.client = APIClient() + self.teacher = GebruikerFactory.create(is_lesgever=True) + self.student = GebruikerFactory.create(is_lesgever=False) + self.template = TemplateFactory.create(user=self.teacher.user) + self.client.force_login(self.teacher.user) + self.template_list_url = reverse("template_list") + + def test_template_list_get(self): + response = self.client.get(self.template_list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Template.objects.count()) + + def test_template_list_get_as_student(self): + self.client.force_login(self.student.user) + response = self.client.get(self.template_list_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_template_list_get_with_teacher(self): + response = self.client.get( + self.template_list_url, {"lesgever_id": self.teacher.user.id} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_template_list_get_with_invalid_teacher(self): + response = self.client.get(self.template_list_url, {"lesgever_id": "invalid"}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_template_list_post(self): + data = { + "user": self.template.user.id, + } + file = SimpleUploadedFile("template.txt", b"file_content") + data["bestand"] = file + response = self.client.post(self.template_list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Template.objects.count(), 2) + + def test_template_list_post_as_student(self): + self.client.force_login(self.student.user) + data = { + "user": self.template.user.id, + } + file = SimpleUploadedFile("template.txt", b"file_content") + data["bestand"] = file + response = self.client.post(self.template_list_url, data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Template.objects.count(), 1) + + def test_template_list_post_with_invalid_data(self): + data = { + "user": self.template.user.id, + } + response = self.client.post(self.template_list_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Template.objects.count(), 1) + + +class TemplateDetailViewTest(APITestCase): + def setUp(self): + self.client = APIClient() + self.template = TemplateFactory.create() + self.teacher = GebruikerFactory.create(is_lesgever=True) + self.student = GebruikerFactory.create(is_lesgever=False) + self.client.force_login(self.teacher.user) + self.template_detail_url = reverse( + "template_detail", kwargs={"id": self.template.template_id} + ) + + def test_template_detail_get(self): + response = self.client.get(self.template_detail_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["template_id"], self.template.template_id) + + def test_template_detail_get_as_student(self): + self.client.force_login(self.student.user) + response = self.client.get(self.template_detail_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_template_detail_get_invalid(self): + response = self.client.get(reverse("template_detail", kwargs={"id": 6969})) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_template_detail_put(self): + data = { + "user": self.template.user.id, + } + file = SimpleUploadedFile("template.txt", b"file_content") + data["bestand"] = file + response = self.client.put(self.template_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["template_id"], self.template.template_id) + + def test_template_detail_patch(self): + data = {"bestand": SimpleUploadedFile("template.txt", b"different_content")} + response = self.client.patch(self.template_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["template_id"], self.template.template_id) + + def test_template_detail_put_as_student(self): + self.client.force_login(self.student.user) + data = { + "user": self.template.user.id, + } + file = SimpleUploadedFile("template.txt", b"file_content") + data["bestand"] = file + response = self.client.put(self.template_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_template_detail_put_with_invalid_data(self): + data = { + "user": self.template.user.id, + } + response = self.client.put(self.template_detail_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_template_detail_delete(self): + response = self.client.delete(self.template_detail_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Template.objects.count(), 0) + + +class TemplateDetailBestandViewTest(APITestCase): + def setUp(self): + self.client = APIClient() + self.template = TemplateFactory.create() + self.teacher = GebruikerFactory.create(is_lesgever=True) + self.student = GebruikerFactory.create(is_lesgever=False) + self.client.force_login(self.teacher.user) + self.template_detail_bestand_url = reverse( + "template_detail_bestand", kwargs={"id": self.template.template_id} + ) + + def test_template_detail_bestand_get(self): + response = self.client.get(self.template_detail_bestand_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(isinstance(response, FileResponse)) + response_content = b"".join(response.streaming_content) + self.template.bestand.open() + template_content = self.template.bestand.read() + self.template.bestand.close() + self.assertEqual(response_content, template_content) + + def test_template_detail_bestand_get_as_student(self): + self.client.force_login(self.student.user) + response = self.client.get(self.template_detail_bestand_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_template_detail_bestand_get_invalid(self): + response = self.client.get( + reverse("template_detail_bestand", kwargs={"id": 6969}) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_template_detail_bestand_get_content_true(self): + response = self.client.get( + self.template_detail_bestand_url, {"content": "true"} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.template.bestand.open() + self.assertEqual(response.data["content"], self.template.bestand.read()) + self.template.bestand.close() + + def test_template_detail_bestand_get_content_false(self): + response = self.client.get( + self.template_detail_bestand_url, {"content": "false"} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(isinstance(response, FileResponse)) + response_content = b"".join(response.streaming_content) + self.template.bestand.open() + template_content = self.template.bestand.read() + self.template.bestand.close() + self.assertEqual(response_content, template_content) diff --git a/api/views/template.py b/api/views/template.py index 39498d4b8..52508e7d0 100644 --- a/api/views/template.py +++ b/api/views/template.py @@ -47,7 +47,8 @@ def template_list(request, format=None): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - return Response(status=status.HTTP_403_FORBIDDEN) + + return Response(status=status.HTTP_403_FORBIDDEN) @api_view(["GET", "PUT", "PATCH", "DELETE"]) @@ -112,10 +113,10 @@ def template_detail_bestand(request, id, format=None): if has_permissions(request.user): if request.method == "GET": bestand = template.bestand.open() - if "content" in request.GET and request.GET.get("content").lower() in [ - "true", - "false", - ]: + if ( + "content" in request.GET + and request.GET.get("content").lower() == "true" + ): return Response({"content": bestand.read()}) return FileResponse(bestand, as_attachment=True) return Response(status=status.HTTP_403_FORBIDDEN) diff --git a/frontend/frontend/cypress/e2e/1-getting-started/todo.cy.js b/frontend/frontend/cypress/e2e/1-getting-started/todo.cy.js new file mode 100644 index 000000000..4768ff923 --- /dev/null +++ b/frontend/frontend/cypress/e2e/1-getting-started/todo.cy.js @@ -0,0 +1,143 @@ +/// + +// Welcome to Cypress! +// +// This spec file contains a variety of sample tests +// for a todo list app that are designed to demonstrate +// the power of writing tests in Cypress. +// +// To learn more about how Cypress works and +// what makes it such an awesome testing tool, +// please read our getting started guide: +// https://on.cypress.io/introduction-to-cypress + +describe('example to-do app', () => { + beforeEach(() => { + // Cypress starts out with a blank slate for each test + // so we must tell it to visit our website with the `cy.visit()` command. + // Since we want to visit the same URL at the start of all our tests, + // we include it in our beforeEach function so that it runs before each test + cy.visit('https://example.cypress.io/todo') + }) + + it('displays two todo items by default', () => { + // We use the `cy.get()` command to get all elements that match the selector. + // Then, we use `should` to assert that there are two matched items, + // which are the two default items. + cy.get('.todo-list li').should('have.length', 2) + + // We can go even further and check that the default todos each contain + // the correct text. We use the `first` and `last` functions + // to get just the first and last matched elements individually, + // and then perform an assertion with `should`. + cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') + cy.get('.todo-list li').last().should('have.text', 'Walk the dog') + }) + + it('can add new todo items', () => { + // We'll store our item text in a variable so we can reuse it + const newItem = 'Feed the cat' + + // Let's get the input element and use the `type` command to + // input our new list item. After typing the content of our item, + // we need to type the enter key as well in order to submit the input. + // This input has a data-test attribute so we'll use that to select the + // element in accordance with best practices: + // https://on.cypress.io/selecting-elements + cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) + + // Now that we've typed our new item, let's check that it actually was added to the list. + // Since it's the newest item, it should exist as the last element in the list. + // In addition, with the two default items, we should have a total of 3 elements in the list. + // Since assertions yield the element that was asserted on, + // we can chain both of these assertions together into a single statement. + cy.get('.todo-list li') + .should('have.length', 3) + .last() + .should('have.text', newItem) + }) + + it('can check off an item as completed', () => { + // In addition to using the `get` command to get an element by selector, + // we can also use the `contains` command to get an element by its contents. + // However, this will yield the