From f3d3720563cf54d7e730e59b0a5964d663bc12b5 Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Tue, 9 Oct 2018 16:14:38 +0200 Subject: [PATCH] Getting frontend e2e tests working --- .gitignore | 1 + .gitlab-ci.yml | 58 +++++++-- core/serializers/tutor.py | 4 +- frontend/src/api.ts | 2 +- frontend/src/components/RegisterDialog.vue | 8 +- frontend/src/pages/Login.vue | 2 +- functional_tests/__init__.py | 0 functional_tests/test_login_page.py | 137 +++++++++++++++++++++ functional_tests/util.py | 5 + grady/settings/default.py | 1 + requirements.dev.txt | 1 + 11 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 functional_tests/__init__.py create mode 100644 functional_tests/test_login_page.py create mode 100644 functional_tests/util.py diff --git a/.gitignore b/.gitignore index a1d7ae81..ed444c0c 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 37c9e1de..aebff399 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 0bcd8e61..faf67477 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 1d081916..d0453302 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 62be92cb..5687f055 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 d949e06b..5185811a 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 00000000..e69de29b diff --git a/functional_tests/test_login_page.py b/functional_tests/test_login_page.py new file mode 100644 index 00000000..b564cc76 --- /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 00000000..2e67d9f0 --- /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 d6da2a26..2dd99ba6 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 eaa909a4..c4009fe1 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 -- GitLab