diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6ea320b..897cd87 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,12 +1,24 @@ stages: + - setup - lint - test - publish - deploy +install: + image: node:12 + stage: setup + script: + - npm ci + artifacts: + paths: + - node_modules + expire_in: 1 week + test: image: python:3.7 stage: test + needs: [] services: - postgres:12 variables: @@ -25,6 +37,7 @@ test: lint:pycodestyle: image: python:3.7 stage: lint + needs: [] script: - pip install pycodestyle - pycodestyle --config=setup.cfg --count . @@ -32,22 +45,21 @@ lint:pycodestyle: lint:eslint: image: node:alpine stage: lint - before_script: - - npm install + needs: ["install"] script: - npm run eslint lint:remark: image: node:alpine stage: lint - before_script: - - npm install + needs: ["install"] script: - npm run remark publish: stage: publish image: docker:stable + needs: ["test", "lint:pycodestyle", "lint:eslint", "lint:remark"] services: - docker:stable-dind only: diff --git a/requirements.txt b/requirements.txt index e46e89a..9abc3cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ django-auditlog==0.4.5 phonenumbers==8.11.4 django-phonenumber-field[phonenumbers]==4.0.0 django-autocomplete-light==3.4.1 -six==1.10.0 +six==1.11.0 django-suit==0.2.26 telepot==12.3 pyexcel==0.5.14 @@ -39,3 +39,4 @@ openpyxl==2.6.4 django-app-namespace-template-loader==0.4.1 django-filter==2.0.0 whitenoise==4.1.4 +jsonschema==3.2.0 diff --git a/webapp/models.py b/webapp/models.py index c84bdd4..0184260 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -100,6 +100,54 @@ class SignupForm(models.Model): def __str__(self): return _('#{} {}').format(self.id, self.title) + @property + def schema(self): + questions = self.questions + properties = {} + + for q in questions: + id = q["id"] + question_type = q["type"] + if question_type == "text": + properties[id] = { + "type": "string" + } + elif question_type == "radiobutton": + options = q["options"] + regexes = map(lambda x: f"^{x}$", options) + pattern = "|".join(regexes) + properties[id] = { + "type": "string", + # Remove last regex or + "pattern": pattern, + + } + elif question_type == "checkbox": + options = q["options"] + regexes = map(lambda x: f"^{x}$", options) + pattern = "|".join(regexes) + + properties[id] = { + "type": "array", + "uniqueItems": True, + "maxItems": len(options), + "items": { + "type": "string", + "pattern": pattern + } + } + else: + raise Exception("invalid question type!") + + ids = list(map(lambda x: x["id"], questions)) + return { + "type": "object", + "required": ids, + "minProperties": len(ids), + "maxProperties": len(ids), + "properties": properties + } + class Meta: verbose_name = _('Signup form') verbose_name_plural = _('Signup forms') diff --git a/webapp/serializers.py b/webapp/serializers.py index 25517ae..765f40a 100644 --- a/webapp/serializers.py +++ b/webapp/serializers.py @@ -3,7 +3,7 @@ from webapp.models import * class SignupFormSerializer(serializers.HyperlinkedModelSerializer): - questions = serializers.JSONField(binary=True) + questions = serializers.JSONField() class Meta: model = SignupForm @@ -56,7 +56,7 @@ class SignupSerializer(serializers.ModelSerializer): source="signupForm", queryset=SignupForm.objects.all() ) - answer = serializers.JSONField(binary=True) + answer = serializers.JSONField() class Meta: model = Signup @@ -69,7 +69,7 @@ class SignupSerializer(serializers.ModelSerializer): class SavedQuestionsSerializer(serializers.ModelSerializer): - question = serializers.JSONField(binary=True) + question = serializers.JSONField() class Meta: model = TemplateQuestion diff --git a/webapp/tests/signup_fixture.py b/webapp/tests/signup_fixture.py index a036274..1ba09f1 100644 --- a/webapp/tests/signup_fixture.py +++ b/webapp/tests/signup_fixture.py @@ -10,7 +10,7 @@ ALL_QUESTION_TYPES = [ {"id": "i10d426d5", "name": "Asd3", "type": "checkbox", "options": ["A", "B", "C"]} ] -ALL_QUESTION_TYPES_ANSWER = {"j5CeRZDvl": "Testi", "RHJhSoaLD": "maybe", "i10d426d5": ["B"]} +ALL_QUESTION_TYPES_ANSWER = {"j5CeRZDvl": "Testi", "RHJhSoaLD": "maybe", "i10d426d5": ["B", "C"]} def createSignupForm(name="Form1", start_time=timezone.now(), end_time=month_from_now(), questions=ALL_QUESTION_TYPES, visible=True): @@ -30,8 +30,8 @@ def createSignupObject(form, answer): ) -def createSignupJSON(form_id, answer): +def createSignupRequest(form_id, answer): return { "signupForm_id": form_id, - "answer": json.dumps(answer) + "answer": answer } diff --git a/webapp/tests/test_signup.py b/webapp/tests/test_signup.py index 66b0427..729f9a4 100644 --- a/webapp/tests/test_signup.py +++ b/webapp/tests/test_signup.py @@ -1,14 +1,10 @@ -from django.test import TestCase from unittest import skip from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase, force_authenticate - -from webapp.serializers import SignupSerializer, SignupFormSerializer +from webapp.serializers import SignupSerializer from webapp.models import Signup -from webapp.tests.event_fixture import createEventObject -from webapp.tests.signup_fixture import createSignupForm, createSignupObject, createSignupJSON, ALL_QUESTION_TYPES, ALL_QUESTION_TYPES_ANSWER - +from webapp.tests.signup_fixture import createSignupForm, createSignupObject, createSignupRequest, ALL_QUESTION_TYPES, ALL_QUESTION_TYPES_ANSWER URL = "/api/signup/" @@ -17,10 +13,13 @@ class SignupTestCase(APITestCase): def setUp(self): self.signupForm = createSignupForm() + self.signupFormText = createSignupForm(name="Form2", questions=[ALL_QUESTION_TYPES[0]]) + self.signupFormRadio = createSignupForm(name="Form3", questions=[ALL_QUESTION_TYPES[1]]) + self.signupFormCheck = createSignupForm(name="Form4", questions=[ALL_QUESTION_TYPES[2]]) self.hiddenForm = createSignupForm(visible=False) self.signup1 = createSignupObject(self.signupForm, ALL_QUESTION_TYPES) - self.signup2 = createSignupObject(self.signupForm, []) + self.signup2 = createSignupObject(self.signupForm, ALL_QUESTION_TYPES) username, password = "test_admin", "password123" self.authClient = User.objects.create_superuser(username, "myemail@test.com", password) @@ -30,10 +29,6 @@ class SignupTestCase(APITestCase): self.signupForm.signup_set.all(), many=True ) - - # Unauthorized - response = self.client.get(URL, format="json") - self.assertTrue(response.status_code, status.HTTP_401_UNAUTHORIZED) # Authenticate self.client.force_authenticate(user=self.authClient) response = self.client.get(URL, format="json") @@ -45,10 +40,6 @@ class SignupTestCase(APITestCase): expected = SignupSerializer( Signup.objects.get(id=id) ) - - # Unauthorized - response = self.client.get(f"{URL}{id}/", format="json") - self.assertTrue(response.status_code, status.HTTP_401_UNAUTHORIZED) # Authenticate self.client.force_authenticate(user=self.authClient) response = self.client.get(f"{URL}{id}/", format="json") @@ -56,32 +47,15 @@ class SignupTestCase(APITestCase): self.assertEqual(response.data, expected.data) def test_create_signup(self): - new = createSignupJSON(self.signupForm.id, ALL_QUESTION_TYPES_ANSWER) + new = createSignupRequest(self.signupForm.id, ALL_QUESTION_TYPES_ANSWER) response = self.client.post(URL, new, format="json") - print(response.data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Signup.objects.count(), 3) - def test_create_signup_404_or_hidden(self): - new = createSignupJSON(3001, ALL_QUESTION_TYPES_ANSWER) - response = self.client.post(URL, new, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Signup.objects.count(), 2) - - new = createSignupJSON(self.hiddenForm.id, ALL_QUESTION_TYPES_ANSWER) - response = self.client.post(URL, new, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Signup.objects.count(), 2) - @skip("NotImplemented") def test_get_hidden_forms_admin(self): pass - @skip("NotImplemented") - def test_create_malformed_answer(self): - response = self.client.post(URL, createSignupJSON(self.signupForm.id, []), format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - # Update and Delete are available for super admin (Django Admin) # and to the user that signed up (uid token) @skip("NotImplemented") diff --git a/webapp/tests/test_signup_errors.py b/webapp/tests/test_signup_errors.py new file mode 100644 index 0000000..7cb4867 --- /dev/null +++ b/webapp/tests/test_signup_errors.py @@ -0,0 +1,95 @@ +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase, force_authenticate +from webapp.serializers import SignupSerializer +from webapp.models import Signup +from webapp.tests.signup_fixture import createSignupForm, createSignupObject, createSignupRequest, ALL_QUESTION_TYPES, ALL_QUESTION_TYPES_ANSWER + +URL = "/api/signup/" + + +class SignupErrorTestCase(APITestCase): + def setUp(self): + self.signupForm = createSignupForm() + self.signupFormText = createSignupForm(name="Form2", questions=[ALL_QUESTION_TYPES[0]]) + self.signupFormRadio = createSignupForm(name="Form3", questions=[ALL_QUESTION_TYPES[1]]) + self.signupFormCheck = createSignupForm(name="Form4", questions=[ALL_QUESTION_TYPES[2]]) + self.hiddenForm = createSignupForm(visible=False) + + self.signup1 = createSignupObject(self.signupForm, ALL_QUESTION_TYPES) + self.signup2 = createSignupObject(self.signupForm, ALL_QUESTION_TYPES) + + username, password = "test_admin", "password123" + self.authClient = User.objects.create_superuser(username, "myemail@test.com", password) + + def test_get_all_unauthorized(self): + response = self.client.get(URL, format="json") + self.assertTrue(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_single_unauthorized(self): + id = self.signup1.id + expected = SignupSerializer( + Signup.objects.get(id=id) + ) + response = self.client.get(f"{URL}{id}/", format="json") + self.assertTrue(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_create_signup_404(self): + new = createSignupRequest(3001, ALL_QUESTION_TYPES_ANSWER) + response = self.client.post(URL, new, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Signup.objects.count(), 2) + + def test_create_signup_hidden(self): + new = createSignupRequest(self.hiddenForm.id, ALL_QUESTION_TYPES_ANSWER) + response = self.client.post(URL, new, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Signup.objects.count(), 2) + + def test_create_empty_body(self): + response = self.client.post(URL, createSignupRequest(self.signupForm.id, {}), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_array_body(self): + response = self.client.post(URL, createSignupRequest(self.signupForm.id, []), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_extra_body(self): + testInput = ALL_QUESTION_TYPES_ANSWER.copy() + testInput["newId"] = "Oon extraa" + response = self.client.post(URL, createSignupRequest(self.signupForm.id, testInput), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_id(self): + response = self.client.post(URL, createSignupRequest(self.signupFormText.id, {"malformed": "TekstiƤ"}), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_type_text(self): + response = self.client.post(URL, createSignupRequest(self.signupFormText.id, {"j5CeRZDvl": 123}), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_data_checkbox(self): + response = self.client.post(URL, createSignupRequest(self.signupFormCheck.id, { + "i10d426d5": ["D"] + }), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_type_checkbox(self): + response = self.client.post(URL, createSignupRequest(self.signupFormCheck.id, { + "i10d426d5": {"j5CeRZDvl": {"asd": "123"}} + }), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_radio(self): + response = self.client.post(URL, createSignupRequest(self.signupFormRadio.id, { + "RHJhSoaLD": [] + }), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_bad_type_radio(self): + response = self.client.post(URL, createSignupRequest(self.signupFormRadio.id, { + "RHJhSoaLD": {"asd": "123"} + }), format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertEqual(Signup.objects.count(), 2) diff --git a/webapp/views.py b/webapp/views.py index b07bd1b..0890501 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -1,19 +1,24 @@ """Webapp views.""" import jwt +import json from django.utils import timezone from dealer.git import git from django.conf import settings from django.contrib.auth import authenticate -from django.http import HttpResponseBadRequest, HttpResponse +from django.http import HttpResponseBadRequest, HttpResponse, JsonResponse from django.shortcuts import redirect, render from django.views.decorators.http import require_http_methods from django_filters import rest_framework as filters +from django.core.exceptions import ObjectDoesNotExist from rest_framework import permissions, routers, viewsets from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from rest_framework.reverse import reverse +from jsonschema import validate +from jsonschema.exceptions import ValidationError +import logging from webapp.models import Event, SignupForm, Signup, TemplateQuestion, Feed, Committee, Occupation, Tag from webapp.serializers import (EventSerializer, SignupFormSerializer, SignupSerializer, @@ -78,13 +83,19 @@ class SignupViewSet(viewsets.ModelViewSet): def create(self, request, *args, **kwargs): try: - form = SignupForm.objects.get(id=request.data["signupForm_id"]) + id = request.data["signupForm_id"] + answer = request.data["answer"] + form = SignupForm.objects.get(id=id) if (form.visible): + # Throws ValidationError if not valid + validate(instance=answer, schema=form.schema) return super().create(request, *args, **kwargs) - except: - return HttpResponseBadRequest() + except ValidationError as inst: + return JsonResponse(status=400, data={"error": inst.message}) + except ObjectDoesNotExist: + return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) else: - return HttpResponseBadRequest() + return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) def update(self, request, *args, **kwargs): return super().update(request, *args, **kwargs)