diff --git a/.gitignore b/.gitignore index a1d7ae810fef9f73c523f0142284fcdf2be2d864..ed444c0ce60790240ee8ec62f24b6b8475010158 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ coverage_html/ .vscode/ anon-export/ public/ +geckodriver.log # node node_modules diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 37c9e1ded60d636729fe151333a1557219cb0a5b..aebff399d3c58c9d05a1d206ef27043cb2329a9b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ stages: - build - test - build_image + - test_build - pages - staging @@ -25,6 +26,27 @@ build_test_env: key: "$CI_JOB_NAME" paths: - .venv + tags: + - docker + +build_frontend: + image: node:carbon + stage: build + script: + - cd frontend + - yarn + - yarn build + artifacts: + paths: + - frontend/dist + expire_in: 20 minutes + cache: + key: "$CI_JOB_NAME" + paths: + - frontend/dist + - frontend/node_modules/ + tags: + - docker # ============================== Testing section ============================= # # ----------------------------- Backend subsection --------------------------- # @@ -34,6 +56,8 @@ build_test_env: - source .venv/bin/activate dependencies: - build_test_env + tags: + - docker test_pytest: <<: *test_definition_virtualenv @@ -58,32 +82,43 @@ test_flake8: # ----------------------------- Frontend subsection -------------------------- # .test_template_frontend: &test_definition_frontend - image: crbanman/nightwatch - when: manual + image: docker.gitlab.gwdg.de/robinwilliam.hundt/python-geckodriver:master before_script: - - cd frontend/ + - source .venv/bin/activate + dependencies: + - build_test_env + - build_frontend + tags: + - docker test_frontend: <<: *test_definition_frontend -# when: manual stage: test + services: + - postgres:9.6 script: - - yarn install - - yarn test:e2e - cache: - key: "$CI_JOB_NAME" - paths: - - frontend/node_modules/ + - cp frontend/dist/index.html core/templates + - python util/format_index.py + - python manage.py collectstatic --no-input + - HEADLESS_TESTS=True pytest --ds=grady.settings.test functional_tests + # =========================== Build Image section ============================ # build_backend: image: docker:latest stage: build_image + services: + - docker:dind + variables: + DOCKER_HOST: tcp://docker:2375/ + DOCKER_DRIVER: overlay2 script: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - docker build -t $IMAGE_TAG . - docker tag $IMAGE_TAG $IMAGE_TAG-$CI_COMMIT_SHA - docker push $IMAGE_TAG + tags: + - docker # =========================== Gitlab pages section =========================== # pages: @@ -101,6 +136,7 @@ pages: only: - master + # ============================== Staging section ============================= # .staging_template: &staging_definition stage: staging @@ -109,6 +145,8 @@ pages: - master before_script: - apk add --update py-pip && pip install docker-compose + tags: + - grady-staging staging: <<: *staging_definition diff --git a/core/serializers/tutor.py b/core/serializers/tutor.py index 0bcd8e61219319829f87c336ac70e28437bff063..faf674777659462ea32fee4bb393fbca31a9a264 100644 --- a/core/serializers/tutor.py +++ b/core/serializers/tutor.py @@ -31,7 +31,9 @@ class TutorSerializer(DynamicFieldsModelSerializer): return t.feedback_validated if hasattr(t, 'feedback_validated') else 0 def create(self, validated_data) -> models.UserAccount: - log.info("Crating tutor from data %s", validated_data) + log_validated_data = dict(validated_data) + log_validated_data['password'] = '******' + log.info("Crating tutor from data %s", log_validated_data) return user_factory.make_tutor( username=validated_data['username'], password=validated_data.get('password'), diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1d081916116a950f5183533f96fc691cca58dbbf..d0453302fdcb83275e03048673d58d316c65fa7c 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -15,7 +15,7 @@ import { function getInstanceBaseUrl (): string { if (process.env.NODE_ENV === 'production') { - return `https://${window.location.host}${window.location.pathname}`.replace(/\/+$/, '') + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`.replace(/\/+$/, '') } else { return 'http://localhost:8000/' } diff --git a/frontend/src/components/RegisterDialog.vue b/frontend/src/components/RegisterDialog.vue index 62be92cb96f97b29ce61b797cfcd258260f5f615..5687f055f61f0909407d3b4a29f8e87b8e48abb4 100644 --- a/frontend/src/components/RegisterDialog.vue +++ b/frontend/src/components/RegisterDialog.vue @@ -4,10 +4,10 @@ Datenschutzerklärung </v-card-title> <v-card-text> - <GDPRNotice/> + <GDPRNotice id="gdpr-notice"/> </v-card-text> <v-card-actions> - <v-btn @click="acceptedGDPR = true">Einwilligen</v-btn> + <v-btn @click="acceptedGDPR = true" id="accept-gdpr-notice">Einwilligen</v-btn> </v-card-actions> </v-card> <v-card v-else> @@ -20,16 +20,18 @@ required autofocus v-model="credentials.username" + id="input-register-username" /> <v-text-field label="Password" required type="password" v-model="credentials.password" + id="input-register-password" /> </v-card-text> <v-card-actions class="justify-center"> - <v-btn flat :loading="loading" @click="register">submit</v-btn> + <v-btn flat :loading="loading" @click="register" id="register-submit">submit</v-btn> </v-card-actions> </v-card> </template> diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index d949e06b97ef032ac5377fac0ff28a526a312458..5185811ad71324a2e7922bd0752c1811109bd959 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -30,7 +30,7 @@ type="password" required /> - <v-btn @click="registerDialog = true">register</v-btn> + <v-btn @click="registerDialog = true" id="register">register</v-btn> <v-btn :loading="loading" type="submit" color="primary">Access</v-btn> </v-form> </v-flex> diff --git a/functional_tests/__init__.py b/functional_tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/functional_tests/test_login_page.py b/functional_tests/test_login_page.py new file mode 100644 index 0000000000000000000000000000000000000000..b564cc7627070c6c47ffb36168e162632014756f --- /dev/null +++ b/functional_tests/test_login_page.py @@ -0,0 +1,137 @@ +import os +import time + +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.firefox.options import Options + +from core.models import UserAccount +from functional_tests.util import get_frontend_url +from util.factories import make_test_data + + +LiveServerTestCase.port = int(os.environ.get('LIVE_SERVER_PORT', 0)) + + +class LoginPageTest(LiveServerTestCase): + def setUp(self): + self.live_server_url = get_frontend_url(self.live_server_url) + options = Options() + # funnily the method is marked deprecated but the alternative setter is not working... + options.headless = bool(os.environ.get('HEADLESS_TESTS', False)) + self.browser = webdriver.Firefox(options=options) + self.browser.implicitly_wait(5) + self.test_data = make_test_data(data_dict={ + 'submission_types': [ + { + 'name': '01. Sort this or that', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '02. Merge this or that or maybe even this', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + } + ], + 'students': [ + {'username': 'student01', 'password': 'p'}, + {'username': 'student02', 'password': 'p'} + ], + 'tutors': [ + {'username': 'tutor01', 'password': 'p'}, + {'username': 'tutor02', 'password': 'p'} + ], + 'reviewers': [ + {'username': 'reviewer', 'password': 'p'} + ], + 'submissions': [ + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' lorem ipsum und so\n', + 'type': '01. Sort this or that', + 'user': 'student01', + 'feedback': { + 'score': 5, + 'is_final': True, + 'feedback_lines': { + '1': [{ + 'text': 'This is very bad!', + 'of_tutor': 'tutor01' + }], + } + + } + }, + { + 'text': 'function blabl\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '02. Merge this or that or maybe even this', + 'user': 'student01' + }, + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '01. Sort this or that', + 'user': 'student02' + }, + { + 'text': 'function lorem ipsum etc\n', + 'type': '02. Merge this or that or maybe even this', + 'user': 'student02' + }, + ]} + ) + + def tearDown(self): + self.browser.quit() + + def _login(self, account): + self.browser.get(self.live_server_url) + username_input = self.browser.find_element_by_xpath('//input[@aria-label="Username"]') + username_input.send_keys(account.username) + password_input = self.browser.find_element_by_xpath('//input[@aria-label="Password"]') + password_input.send_keys('p') + self.browser.find_element_by_xpath('//button[@type="submit"]').send_keys(Keys.ENTER) + time.sleep(1) + + def test_tutor_can_login(self): + tutor = self.test_data['tutors'][0] + self._login(tutor) + self.assertTrue(self.browser.current_url.endswith('#/home')) + + def test_reviewer_can_login(self): + reviewer = self.test_data['reviewers'][0] + self._login(reviewer) + self.assertTrue(self.browser.current_url.endswith('#/home')) + + def test_student_can_login(self): + student = self.test_data['students'][0] + self._login(student) + self.assertTrue(self.browser.current_url.endswith('#/home')) + + def test_can_register_account(self): + username = 'danny' + password = 'redrum-is-murder-reversed' + self.browser.get(self.live_server_url) + self.browser.find_element_by_id('register').click() + self.browser.find_element_by_id('gdpr-notice') + self.browser.find_element_by_id('accept-gdpr-notice').click() + username_input = self.browser.find_element_by_id('input-register-username') + username_input.send_keys(username) + password_input = self.browser.find_element_by_id('input-register-password') + password_input.send_keys(password) + self.browser.find_element_by_id('register-submit').click() + time.sleep(1) + tutor = UserAccount.objects.get(username=username) + self.assertEqual(UserAccount.TUTOR, tutor.role) + self.assertFalse(tutor.is_active, "Tutors should be inactive after registered") + diff --git a/functional_tests/util.py b/functional_tests/util.py new file mode 100644 index 0000000000000000000000000000000000000000..2e67d9f09df8293470cd3a3d240be2c9ae24ddb6 --- /dev/null +++ b/functional_tests/util.py @@ -0,0 +1,5 @@ +import os + + +def get_frontend_url(live_server_url): + return os.environ.get('FRONTEND_URL', live_server_url) diff --git a/grady/settings/default.py b/grady/settings/default.py index d6da2a266330052d52b892b25923ccf50bf04ecc..2dd99ba6223153554cc8a34bcdbc7502e7e21c70 100644 --- a/grady/settings/default.py +++ b/grady/settings/default.py @@ -127,6 +127,7 @@ AUTH_USER_MODEL = 'core.UserAccount' AUTH_PASSWORD_VALIDATORS = [] CORS_ORIGIN_WHITELIST = ( 'localhost:8080' + 'localhost:8000' ) REST_FRAMEWORK = { diff --git a/requirements.dev.txt b/requirements.dev.txt index eaa909a4ee73cc11378a4b7d2041c5d66d204e48..c4009fe1e04d55308a743d84612ba2b271dfced7 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -2,3 +2,4 @@ flake8~=3.5.0 pre-commit~=1.4.1 pytest-cov~=2.5.1 pytest-django~=3.1.2 +selenium~=3.14.1