diff --git a/client/cypress/integration/change-password-spec.js b/client/cypress/integration/change-password-spec.js new file mode 100644 index 00000000..e5b623dc --- /dev/null +++ b/client/cypress/integration/change-password-spec.js @@ -0,0 +1,95 @@ +describe('Change Password Page', () => { + + const validNewPassword = 'Abcd1234!'; + const validOldPassword = 'test'; + const validationTooShort = 'Das neue Passwort muss mindestens 8 Zeichen lang sein'; + const validationErrorMsg = 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten'; + const validationOldWrongMsg = 'Die Eingabe ist falsch'; + + after(function () { + cy.exec("python ../server/manage.py reset_testuser_password rahel.cueni"); + }); + + it('shows an empty form', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.get('[data-cy=password-change-success]').should('not.exist'); + cy.get('[data-cy=old-password]').should('have.value', ''); + cy.get('[data-cy=new-password]').should('have.value', ''); + }); + + it('shows errors if old password is not entered', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword('', validNewPassword); + cy.get('[data-cy=old-password-local-errors]').should('contain', 'Dein aktuelles Passwort fehlt') + }); + + it('shows errors if new password is not entered', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, ''); + cy.get('[data-cy=new-password-local-errors]').should('contain', 'Dein neues Passwort fehlt') + }); + + it('shows errors if new password is too short', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, 'Abc1!'); + cy.get('[data-cy=new-password-local-errors]').should('contain', validationTooShort) + }); + + it('shows errors if new password has no uppercase letter', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, 'aabdddedddbc1!'); + cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) + }); + + it('shows errors if new password has no lowercase letter', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, 'ABCDDD334551!'); + cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) + }); + + it('shows errors if new password has no digit', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, 'AbcdEEDE!'); + cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) + }); + + it('shows errors if new password has no special character', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, 'AbcdEEDE09877'); + cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) + }); + + it('shows errors if old password does not match', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword('test12345', validNewPassword); + cy.get('[data-cy=old-password-remote-errors]').should('contain', validationOldWrongMsg) + }); + + it('shows success if change was successful', () => { + cy.login('rahel.cueni', 'test'); + cy.visit('/me/password-change'); + + cy.changePassword(validOldPassword, validNewPassword); + cy.get('[data-cy=password-change-success]').should('contain', 'Dein Password wurde erfolgreich geändert.'); + cy.get('[data-cy=old-password]').should('have.value', ''); + cy.get('[data-cy=new-password]').should('have.value', ''); + }); +}); diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js index ebc97c6b..820873dd 100644 --- a/client/cypress/support/commands.js +++ b/client/cypress/support/commands.js @@ -73,3 +73,14 @@ Cypress.Commands.add('waitFor', operationName => { } }); }); + +Cypress.Commands.add('changePassword', (oldPassword, newPassword) => { + if (oldPassword) { + cy.get('[data-cy=old-password]').type(oldPassword); + } + + if (newPassword) { + cy.get('[data-cy=new-password]').type(newPassword); + } + cy.get('[data-cy=change-password-button]').click(); +}); diff --git a/client/package-lock.json b/client/package-lock.json index d6031d7f..815dae07 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11825,6 +11825,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "vee-validate": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-2.2.0.tgz", + "integrity": "sha512-s72VQcl1DWTNQKQyHtUDcU536dIx/GYDnCObDj4AXDZtWnqM3rXbgp7FCT3D2q9HFKw7IykW9bVrClhPBeQnrw==" + }, "vendors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index ba340f11..7ff7c05c 100644 --- a/client/package.json +++ b/client/package.json @@ -69,6 +69,7 @@ "uploadcare-widget": "^3.6.0", "url-loader": "^1.0.1", "uuid": "^3.2.1", + "vee-validate": "^2.2.0", "vue": "^2.5.17", "vue-analytics": "^5.16.2", "vue-apollo": "^3.0.0-beta.16", diff --git a/client/src/App.vue b/client/src/App.vue index 1c73926d..f874cbc6 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,5 +1,5 @@ diff --git a/client/src/pages/myClasses.vue b/client/src/pages/myClasses.vue new file mode 100644 index 00000000..e52ecdfc --- /dev/null +++ b/client/src/pages/myClasses.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/client/src/pages/passwordChange.vue b/client/src/pages/passwordChange.vue new file mode 100644 index 00000000..8c01b0e8 --- /dev/null +++ b/client/src/pages/passwordChange.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/client/src/pages/profile.vue b/client/src/pages/profile.vue new file mode 100644 index 00000000..faf7caa4 --- /dev/null +++ b/client/src/pages/profile.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/client/src/router/index.js b/client/src/router/index.js index e70eedc9..4d3f6a24 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -1,5 +1,4 @@ import Vue from 'vue' -import Router from 'vue-router' // import index from '@/pages/index' import topic from '@/pages/topic' import book from '@/pages/book' @@ -18,6 +17,11 @@ import start from '@/pages/start' import submission from '@/pages/studentSubmission' import portfolio from '@/pages/portfolio' import project from '@/pages/project' +import profilePage from '@/pages/profile' +import passwordChange from '@/pages/passwordChange' +import myClasses from '@/pages/myClasses' +import activity from '@/pages/activity' +import Router from 'vue-router' import editProject from '@/pages/editProject' import newProject from '@/pages/newProject' @@ -73,6 +77,16 @@ const routes = [ {path: 'topic/:topicSlug', component: topic, meta: {subnavigation: true}} ] }, + { + path: '/me', + component: profilePage, + children: [ + {path: 'password-change', name: 'pw-change', component: passwordChange, meta: {isProfile: true}}, + {path: 'myclasses', name: 'my-classes', component: myClasses, meta: {isProfile: true}}, + {path: 'activity', name: 'activity', component: activity, meta: {isProfile: true}}, + {path: '', name: 'profile-activity', component: activity, meta: {isProfile: true}}, + ] + }, {path: '*', component: p404} ]; diff --git a/client/src/store/index.js b/client/src/store/index.js index e8ee6545..2e444a39 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -25,7 +25,10 @@ export default new Vuex.Store({ id: 0, type: '' }, - vimeoId: null + vimeoId: null, + scrollToAssignmentId: '', + scrollToAssignmentReady: false, + scrollingToAssignment: false }, getters: { @@ -34,7 +37,10 @@ export default new Vuex.Store({ }, showMobileNavigation: state => { return state.showMobileNavigation - } + }, + scrollToAssignmentId: state => state.scrollToAssignmentId, + scrollToAssignmentReady: state => state.scrollToAssignmentReady, + scrollingToAssignment: state => state.scrollingToAssignment, }, actions: { @@ -118,6 +124,22 @@ export default new Vuex.Store({ }, showMobileNavigation({commit}, payload) { commit('setShowMobileNavigation', payload); + }, + scrollToAssignmentId({commit}, payload) { + commit('setScrollToAssignmentId', payload); + }, + scrollToAssignmentReady({commit}, payload) { + commit('setScrollToAssignmentReady', payload); + }, + scrollingToAssignment({commit, state, dispatch}, payload) { + if (payload && !state.scrollingToAssignment) { + commit('setScrollingToAssignment', true); + }; + + if (!payload && state.scrollingToAssignment) { + commit('setScrollingToAssignment', false); + dispatch('scrollToAssignmentId', ''); + } } }, @@ -172,6 +194,15 @@ export default new Vuex.Store({ }, setShowMobileNavigation(state, payload) { state.showMobileNavigation = payload; + }, + setScrollToAssignmentId(state, payload) { + state.scrollToAssignmentId = payload; + }, + setScrollToAssignmentReady(state, payload) { + state.scrollToAssignmentReady = payload; + }, + setScrollingToAssignment(state, payload) { + state.scrollingToAssignment = payload; } } }) diff --git a/client/src/styles/_top-navigation.scss b/client/src/styles/_top-navigation.scss new file mode 100644 index 00000000..4c93a283 --- /dev/null +++ b/client/src/styles/_top-navigation.scss @@ -0,0 +1,15 @@ +.top-navigation { + display: flex; + + &__link { + font-size: 1.0625rem; + padding: 0 24px; + font-family: $sans-serif-font-family; + font-weight: $font-weight-regular; + color: $color-grey; + + &--active { + color: $color-brand; + } + } +} diff --git a/client/src/styles/main.scss b/client/src/styles/main.scss index 0f8be63b..87f9760f 100644 --- a/client/src/styles/main.scss +++ b/client/src/styles/main.scss @@ -13,3 +13,4 @@ @import "objective-group"; @import "article"; @import "actions"; +@import "top-navigation"; diff --git a/server/api/schema.py b/server/api/schema.py index dc4d40ec..961cdf35 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -6,7 +6,7 @@ from graphene_django.debug import DjangoDebug # noinspection PyUnresolvedReferences from api import graphene_wagtail # Keep this import exactly here, it's necessary for StreamField conversion from assignments.schema.mutations import AssignmentMutations -from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery +from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery, MyActivityQuery from basicknowledge.queries import BasicKnowledgeQuery from books.schema.mutations.main import BookMutations from books.schema.queries import BookQuery @@ -18,10 +18,11 @@ from portfolio.schema import PortfolioQuery from rooms.mutations import RoomMutations from rooms.schema import RoomsQuery from users.schema import UsersQuery +from users.mutations import ProfileMutations class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, - BasicKnowledgeQuery, PortfolioQuery, graphene.ObjectType): + BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, graphene.ObjectType): node = relay.Node.Field() if settings.DEBUG: @@ -29,7 +30,8 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, - graphene.ObjectType): + ProfileMutations, graphene.ObjectType): + if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='__debug') diff --git a/server/assignments/factories.py b/server/assignments/factories.py index 2c0638e1..7711ff2f 100644 --- a/server/assignments/factories.py +++ b/server/assignments/factories.py @@ -3,10 +3,11 @@ import random import factory from books.factories import ModuleFactory -from .models import Assignment +from .models import Assignment, StudentSubmission from core.factories import fake + class AssignmentFactory(factory.django.DjangoModelFactory): class Meta: model = Assignment @@ -14,3 +15,12 @@ class AssignmentFactory(factory.django.DjangoModelFactory): title = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8))) assignment = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8))) module = factory.SubFactory(ModuleFactory) + + +class StudentSubmissionFactory(factory.django.DjangoModelFactory): + class Meta: + model = StudentSubmission + + text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8))) + assignment = factory.SubFactory(AssignmentFactory) + final = False diff --git a/server/assignments/schema/queries.py b/server/assignments/schema/queries.py index 5bb67e28..7f9ac9da 100644 --- a/server/assignments/schema/queries.py +++ b/server/assignments/schema/queries.py @@ -1,6 +1,7 @@ from graphene import relay from graphene_django.filter import DjangoFilterConnectionField +from assignments.models import StudentSubmission from assignments.schema.types import AssignmentNode, StudentSubmissionNode @@ -12,3 +13,10 @@ class AssignmentsQuery(object): class StudentSubmissionQuery(object): student_submission = relay.Node.Field(StudentSubmissionNode) + +class MyActivityQuery(object): + my_activity = DjangoFilterConnectionField(StudentSubmissionNode) + + def resolve_my_activity(self, info, **kwargs): + user = info.context.user + return StudentSubmission.objects.filter(student=user) diff --git a/server/assignments/schema/types.py b/server/assignments/schema/types.py index 88b57e43..92107b8a 100644 --- a/server/assignments/schema/types.py +++ b/server/assignments/schema/types.py @@ -9,6 +9,7 @@ from books.utils import are_solutions_enabled_for class StudentSubmissionNode(DjangoObjectType): class Meta: model = StudentSubmission + filter_fields = [] interfaces = (relay.Node,) diff --git a/server/assignments/tests/test_myassignments.py b/server/assignments/tests/test_myassignments.py new file mode 100644 index 00000000..f3efd115 --- /dev/null +++ b/server/assignments/tests/test_myassignments.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-04-11 +# @author: chrigu +from django.conf import settings +import json + +from django.test import TestCase, RequestFactory +from graphene.test import Client + +from api import schema +from api.schema import schema +from api.test_utils import DefaultUserTestCase, create_client +from assignments.factories import AssignmentFactory, StudentSubmissionFactory +from assignments.models import Assignment +from books.factories import ModuleFactory +from books.models import ContentBlock, Chapter +from core.factories import UserFactory +from users.models import User +from users.services import create_users + + +class MyAssignemntsText(DefaultUserTestCase): + def setUp(self): + super(MyAssignemntsText, self).setUp() + self.assignment = AssignmentFactory( + owner=self.teacher + ) + + self.submission1 = StudentSubmissionFactory(student=self.student1, assignment=self.assignment) + self.submission2 = StudentSubmissionFactory(student=self.student2, assignment=self.assignment) + + self.client = create_client(self.student1) + + def query_my_assignments(self): + query = ''' + query { + myActivity { + edges { + node { + id + text + assignment { + id + title + } + } + } + } + } + ''' + + result = self.client.execute(query) + + self.assertIsNone(result.get('errors')) + return result + + @staticmethod + def get_content(result): + return result.get('data').get('myActivity').get('edges') + + def test_my_assignment_query(self): + result = self.query_my_assignments() + contents = self.get_content(result) + self.assertEqual(len(contents), 1) + self.assertEquals(contents[0].get('node').get('text'), self.submission1.text) + diff --git a/server/books/schema/queries.py b/server/books/schema/queries.py index c2804209..50471eb9 100644 --- a/server/books/schema/queries.py +++ b/server/books/schema/queries.py @@ -67,13 +67,14 @@ class ChapterNode(DjangoObjectType): class ModuleNode(DjangoObjectType): pk = graphene.Int() chapters = DjangoFilterConnectionField(ChapterNode) + topic = graphene.Field('books.schema.queries.TopicNode') hero_image = graphene.String() solutions_enabled = graphene.Boolean() class Meta: model = Module only_fields = [ - 'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image' + 'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image', 'topic' ] filter_fields = { 'slug': ['exact', 'icontains', 'in'], @@ -91,6 +92,10 @@ class ModuleNode(DjangoObjectType): def resolve_chapters(self, info, **kwargs): return Chapter.get_by_parent(self) + def resolve_topic(self, info, **kwargs): + some = self.get_parent().specific + return self.get_parent().specific + def resolve_solutions_enabled(self, info, **kwargs): return self.solutions_enabled_by.filter(pk=info.context.user.pk).exists() diff --git a/server/core/management/commands/reset_testuser_password.py b/server/core/management/commands/reset_testuser_password.py new file mode 100644 index 00000000..36165cf3 --- /dev/null +++ b/server/core/management/commands/reset_testuser_password.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-04-08 +# @author: chrigu +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.management import BaseCommand + +from books.models import Module + + +class Command(BaseCommand): + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('username', nargs='+', type=str) + + def handle(self, *args, **options): + self.stdout.write("Reset Testuser Password") + + email = "{}@skillbox.example".format(options['username'][0]) + + try: + user = get_user_model().objects.get(email=email) + user.set_password('test') + user.save() + self.stdout.write("Password reset successful") + except get_user_model().DoesNotExist: + self.stdout.write("No user found!") diff --git a/server/rooms/tests/test_room_entry_mutations.py b/server/rooms/tests/test_room_entry_mutations.py index 3146fd7a..d20f5c0a 100644 --- a/server/rooms/tests/test_room_entry_mutations.py +++ b/server/rooms/tests/test_room_entry_mutations.py @@ -8,6 +8,7 @@ from rooms.factories import RoomEntryFactory, RoomFactory from rooms.models import RoomEntry from users.factories import SchoolClassFactory + class RoomEntryMutationsTestCase(TestCase): def setUp(self): self.user = UserFactory(username='aschi') diff --git a/server/users/inputs.py b/server/users/inputs.py index 2440f36c..7956b03b 100644 --- a/server/users/inputs.py +++ b/server/users/inputs.py @@ -6,3 +6,8 @@ class SchoolClassInput(InputObjectType): id = graphene.ID() name = graphene.String() year = graphene.Int() + + +class PasswordUpdateInput(InputObjectType): + old_password = graphene.String() + new_password = graphene.String() diff --git a/server/users/mutations.py b/server/users/mutations.py new file mode 100644 index 00000000..64255ac1 --- /dev/null +++ b/server/users/mutations.py @@ -0,0 +1,49 @@ +import graphene +from django.contrib.auth import update_session_auth_hash +from graphene import relay +from users.inputs import PasswordUpdateInput +from users.serializers import PasswordSerialzer + + +class FieldError(graphene.ObjectType): + code = graphene.String() + + +class UpdateError(graphene.ObjectType): + field = graphene.String() + errors = graphene.List(FieldError) + + +class UpdatePassword(relay.ClientIDMutation): + class Input: + password_input = graphene.Argument(PasswordUpdateInput) + + success = graphene.Boolean() + errors = graphene.List(UpdateError) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + user = info.context.user + password_data = kwargs.get('password_input') + + serializer = PasswordSerialzer(data=password_data, context=user) + + if serializer.is_valid(): + user.set_password(password_data['new_password']) + user.save() + update_session_auth_hash(info.context, user) + return cls(success=True) + + errors = [] + for key, value in serializer.errors.items(): + error = UpdateError(field=key, errors=[]) + for field_error in serializer.errors[key]: + error.errors.append(FieldError(code=field_error.code)) + + errors.append(error) + + return cls(success=False, errors=errors) + + +class ProfileMutations: + update_password = UpdatePassword.Field() diff --git a/server/users/serializers.py b/server/users/serializers.py new file mode 100644 index 00000000..9a9b0cee --- /dev/null +++ b/server/users/serializers.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-04-02 +# @author: chrigu +import re + +from django.contrib.auth import get_user_model +from rest_framework import serializers +from rest_framework.fields import CharField +from django.utils.translation import ugettext_lazy as _ + +MIN_PASSWORD_LENGTH = 8 + +# For future versions https://docs.djangoproject.com/en/2.1/topics/auth/passwords/#integrating-validation + + +def validate_old_password(old_password, username): + user = get_user_model().objects.get(username=username) + if user.check_password(old_password): + return old_password + else: + raise serializers.ValidationError(_(u'Das eingegebene Passwort ist falsch')) + + +def validate_old_new_password(value): + if value.get('old_password') == '' and value.get('new_password') == '': + return value + elif value.get('old_password') == '' and value.get('new_password') != '': + raise serializers.ValidationError(_(u'Das neue Passwort muss gesetzt werden')) + elif value.get('old_password') != '' and value.get('new_password') == '': + raise serializers.ValidationError(_(u'Das alte Passwort muss angegeben werden')) + + return value + + +def validate_strong_email(password): + + has_number = re.search('\d', password) + has_upper = re.search('[A-Z]', password) + has_lower = re.search('[a-z]', password) + has_special = re.search('[!@#$%^&*(),.?":{}|<>\+]', password) + + if has_number and has_upper and has_lower and has_special: + return password + else: + raise serializers.ValidationError(_(u'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten')) + + +class PasswordSerialzer(serializers.Serializer): + old_password = CharField(allow_blank=True) + new_password = CharField(allow_blank=True, min_length=MIN_PASSWORD_LENGTH) + + def validate_new_password(self, value): + return validate_strong_email(value) + + def validate_old_password(self, value): + return validate_old_password(value, self.context.username) + + def validate(self, obj): + return validate_old_new_password(obj) diff --git a/server/users/tests/__init__.py b/server/users/tests/__init__.py new file mode 100644 index 00000000..5c234b8e --- /dev/null +++ b/server/users/tests/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-04-04 +# @author: chrigu +from django.conf import settings diff --git a/server/users/tests/test_my_school_classes.py b/server/users/tests/test_my_school_classes.py new file mode 100644 index 00000000..0499d622 --- /dev/null +++ b/server/users/tests/test_my_school_classes.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-04-09 +# @author: chrigu +from django.conf import settings +from django.test import TestCase, RequestFactory +from graphene.test import Client +from api.schema import schema +from core.factories import UserFactory +from django.contrib.auth import authenticate + +from users.factories import SchoolClassFactory + + +class PasswordUpdate(TestCase): + def setUp(self): + self.user = UserFactory(username='aschi') + self.another_user = UserFactory(username='pesche') + self.class1 = SchoolClassFactory(users=[self.user, self.another_user]) + self.class2 = SchoolClassFactory(users=[self.user]) + self.class3 = SchoolClassFactory(users=[self.another_user]) + + request = RequestFactory().get('/') + request.user = self.user + self.client = Client(schema=schema, context_value=request) + + def make_query(self): + + query = ''' + query { + me { + schoolClasses { + edges { + node { + id + name + users { + edges { + node { + id + firstName + lastName + permissions + } + } + } + } + } + } + } + } + ''' + return self.client.execute(query) + + def test_user_sees_her_classes(self): + + result = self.make_query() + + classes = result.get('data').get('me').get('schoolClasses').get('edges') + self.assertEqual(len(classes), 2) + + for school_class in classes: + if school_class.get('name') == self.class1: + self.assertEqual(len(school_class.get('node')), 2) + elif school_class.get('name') == self.class2: + self.assertEqual(len(school_class.get('node')), 1) + elif school_class.get('name') == self.class2: + self.fail('MySchoolClassTest:test_user_sees_her_classes: Class should not be in response') + + + diff --git a/server/users/tests/test_password_update.py b/server/users/tests/test_password_update.py new file mode 100644 index 00000000..08f67f05 --- /dev/null +++ b/server/users/tests/test_password_update.py @@ -0,0 +1,134 @@ +from django.contrib.sessions.middleware import SessionMiddleware +from django.test import TestCase, RequestFactory +from graphene.test import Client +from api.schema import schema +from core.factories import UserFactory +from django.contrib.auth import authenticate + + +class PasswordUpdate(TestCase): + def setUp(self): + self.user = UserFactory(username='aschi') + + request = RequestFactory().get('/') + request.user = self.user + + # adding session + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + self.client = Client(schema=schema, context_value=request) + + def make_request(self, new_password, old_password='test'): + + mutation = ''' + mutation UpdatePassword($input: UpdatePasswordInput!) { + updatePassword(input: $input) { + success + errors { + field + errors { + code + } + } + } + } + ''' + + return self.client.execute(mutation, variables={ + 'input': { + 'passwordInput': { + 'oldPassword': old_password, + 'newPassword': new_password + } + } + }) + + def test_update_password(self): + + new_password = 'Abcd123!' + result = self.make_request(new_password) + + self.assertTrue(result.get('data').get('updatePassword').get('success')) + + user = authenticate(username=self.user.username, password=new_password) + self.assertIsNotNone(user) + + def test_update_fails_with_short_password(self): + + new_password = 'Ab!d123' + + result = self.make_request(new_password) + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'new_password', + 'errors': [ + {'code': 'min_length'} + ] + }) + + def test_update_fails_with_no_special_character(self): + + new_password = 'Abcd1239' + result = self.make_request(new_password) + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'new_password', + 'errors': [ + {'code': 'invalid'} + ] + }) + + def test_update_fails_with_no_digit(self): + + new_password = 'Abcd!asddfg' + result = self.make_request(new_password) + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'new_password', + 'errors': [ + {'code': 'invalid'} + ] + }) + + def test_update_fails_with_no_lowercase_char(self): + + new_password = '45ABDC!AWSWS' + result = self.make_request(new_password) + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'new_password', + 'errors': [ + {'code': 'invalid'} + ] + }) + + def test_update_fails_with_no_uppercase_char(self): + + new_password = '45aswed!aswdef' + result = self.make_request(new_password) + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'new_password', + 'errors': [ + {'code': 'invalid'} + ] + }) + + def test_update_fails_with_wrong_old_password(self): + + new_password = 'Abcd123!' + result = self.make_request(new_password, 'testttt') + + self.assertFalse(result.get('data').get('updatePassword').get('success')) + self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], { + 'field': 'old_password', + 'errors': [ + {'code': 'invalid'} + ] + })