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