Add login happy paths

This commit is contained in:
Christian Cueni 2020-01-28 08:35:45 +01:00
parent 87ceb5fc0e
commit bc997bbeea
18 changed files with 390 additions and 85 deletions

View File

@ -53,6 +53,7 @@ class UserFactory(factory.django.DjangoModelFactory):
first_name = factory.LazyAttribute(lambda x: fake.first_name())
last_name = factory.LazyAttribute(lambda x: fake.last_name())
email = factory.LazyAttribute(lambda x: fake.ascii_safe_email())
hep_id = factory.Sequence(lambda n: n)
@factory.post_generation
def post(self, create, extracted, **kwargs):

View File

@ -7,12 +7,16 @@
#
# Created on 23.01.20
# @author: chrigu <christian.cueni@iterativ.ch>
from datetime import datetime
from django.conf import settings
import logging
import requests
logger = logging.getLogger(__name__)
MYSKILLBOX_TEACHER_EDITION_ISBN = "000-4-5678-9012-3"
MYSKILLBOX_STUDENT_EDITION_ISBN = "123-4-5678-9012-3"
class HepClientException(Exception):
pass
@ -44,6 +48,7 @@ class HepClient:
else:
response = requests.get(request_url, headers=headers)
# Todo handle 401 and most important network errors
if response.status_code != 200:
raise HepClientException(response.status_code, response.json())
@ -87,7 +92,7 @@ class HepClient:
response = self._call('/rest/V1/customers/me', additional_headers={'authorization': 'Bearer {}'.format(token)})
return response.json()
def customer_orders(self, admin_token, customer_id):
def _customer_orders(self, admin_token, customer_id):
url = ("/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0]["
"field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={}".format(customer_id))
@ -97,3 +102,34 @@ class HepClient:
def coupon_redeem(self, coupon, customer_id):
response = self._call('/rest/deutsch/V1/coupon/{}/customer/{}'.format(coupon, customer_id), method='put')
return response
def myskillbox_product_for_customer(self, admin_token, customer_id):
orders = self._customer_orders(admin_token, customer_id)
# Todo return only relevant product
return self._extract_myskillbox_products(orders)
def _extract_myskillbox_products(self, orders):
# Todo retun all products
product = None
for order_item in orders['items']:
for item in order_item['items']:
if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN:
product = {
'edition': 'teacher',
'raw': item,
'activated': self._get_item_activation(order_item)
}
elif not product and item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN:
product = {
'edition': 'student',
'raw': item,
'activated': self._get_item_activation(order_item)
}
return product
def _get_item_activation(self, item):
for history in item['status_histories']:
if history['comment'] == 'payed by couponcode':
return datetime.strptime(history['created_at'], '%Y-%m-%d %H:%M:%S')

View File

@ -371,3 +371,6 @@ TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL")
USE_LOCAL_REGISTRATION = False
# HEP
HEP_ADMIN_TOKEN = "asdf"

View File

@ -15,3 +15,5 @@ MIGRATION_MODULES = DisableMigrations()
SENDGRID_API_KEY = ""
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
LOGIN_REDIRECT_URL = '/accounts/login/'
USE_LOCAL_REGISTRATION = False

View File

@ -9,17 +9,11 @@
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib import admin
from registration.models import LicenseType, License
@admin.register(LicenseType)
class LicenseTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'key', 'for_role', 'active')
list_filter = ('for_role', 'active')
from registration.models import License
@admin.register(License)
class LicenseAdmin(admin.ModelAdmin):
list_display = ('license_type', 'licensee')
list_filter = ('license_type', 'licensee')
list_display = ('licensee',)
list_filter = ('licensee',)
raw_id_fields = ('licensee',)

View File

@ -11,17 +11,7 @@ import random
import factory
from registration.models import LicenseType, License
class LicenseTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = LicenseType
name = factory.Sequence(lambda n: 'license-{}'.format(n))
active = True
key = factory.Sequence(lambda n: "license-key-%03d" % n)
description = factory.Sequence(lambda n: "Some description %03d" % n)
from registration.models import License
class LicenseFactory(factory.django.DjangoModelFactory):

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 27.01.20
# @author: chrigu <christian.cueni@iterativ.ch>
from datetime import timedelta
from django.db import models
from users.models import Role
TEACHER_EDITION_DURATION = 365
STUDENT_EDITION_DURATION = 4*365
class LicenseManager(models.Manager):
def create_license_for_role(self, licensee, activation_date, raw, role):
if role == 'teacher':
user_role = Role.objects.get_default_teacher_role()
expiry_date = activation_date + timedelta(TEACHER_EDITION_DURATION)
else:
user_role = Role.objects.get_default_student_role()
expiry_date = activation_date + timedelta(STUDENT_EDITION_DURATION)
return self._create_license_for_role(licensee, expiry_date, raw, user_role)
def _create_license_for_role(self, licensee, expiry_date, raw, role):
return self.create(licensee=licensee, expire_date=expiry_date, raw=raw, for_role=role)

View File

@ -0,0 +1,41 @@
# Generated by Django 2.0.6 on 2020-01-27 09:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0010_magentotoken'),
('registration', '0002_auto_20191010_0905'),
]
operations = [
migrations.RemoveField(
model_name='licensetype',
name='for_role',
),
migrations.RemoveField(
model_name='license',
name='license_type',
),
migrations.AddField(
model_name='license',
name='expire_date',
field=models.DateField(null=True),
),
migrations.AddField(
model_name='license',
name='for_role',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.Role'),
),
migrations.AddField(
model_name='license',
name='raw',
field=models.TextField(default=''),
),
migrations.DeleteModel(
name='LicenseType',
),
]

View File

@ -7,28 +7,29 @@
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.db import models
from registration.managers import LicenseManager
from users.managers import RoleManager
from users.models import Role, User
class LicenseType(models.Model):
class License(models.Model):
for_role = models.ForeignKey(Role, blank=False, null=True, on_delete=models.CASCADE)
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
expire_date = models.DateField(blank=False, null=True,)
raw = models.TextField(default="")
name = models.CharField(_('License name'), max_length=255, blank=False, null=False)
for_role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE)
key = models.CharField(max_length=128, blank=False, null=False, unique=True)
active = models.BooleanField(_('License active'), default=False)
description = models.TextField(_('Description'), default="")
objects = LicenseManager()
def is_teacher_license(self):
return self.for_role.key == RoleManager.TEACHER_KEY
def is_valid(self):
return datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day) <= datetime.now()
def __str__(self):
return '%s - role: %s' % (self.name, self.for_role)
class License(models.Model):
license_type = models.ForeignKey(LicenseType, blank=False, null=False, on_delete=models.CASCADE)
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
return 'License for role: %s' % self.for_role

View File

@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.fields import CharField, EmailField
from django.utils.translation import ugettext_lazy as _
from registration.models import License, LicenseType
from registration.models import License
class RegistrationSerializer(serializers.Serializer):
@ -38,9 +38,4 @@ class RegistrationSerializer(serializers.Serializer):
return lower_email
def validate_license_key(self, value):
license_types = LicenseType.objects.filter(key=value, active=True)
if len(license_types) == 0:
raise serializers.ValidationError(_(u'Die Lizenznummer ist ungültig'))
self.context['license_type'] = license_types[0] # Assuming there is just ONE license per key
return value

View File

@ -4,6 +4,8 @@ from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.contrib.auth.models import UserManager as DjangoUserManager
from core.hep_client import HepClient
class RoleManager(models.Manager):
use_in_migrations = True
@ -82,12 +84,27 @@ class UserRoleManager(models.Manager):
class UserManager(DjangoUserManager):
def create_user_with_random_password(self, first_name, last_name, email):
def _create_user_with_random_password_no_save(self, first_name, last_name, email):
user, created = self.model.objects.get_or_create(email=email, username=email)
user.first_name = first_name
user.last_name = last_name
# Todo: remove if not used
# user.set_password(self.model.objects.make_random_password())
user.set_unusable_password()
return user
def create_user_with_random_password(self, first_name, last_name, email):
user = self._create_user_with_random_password_no_save()
user.save()
return user
def create_user_from_hep(self, token):
hep_client = HepClient()
me_data = hep_client.customer_me(token)
user = self.user = self._create_user_with_random_password_no_save(
me_data['firstname'], me_data['lastname'], me_data['email'])
user.hep_id = me_data['id']
user.save()
return user

View File

@ -0,0 +1,24 @@
# Generated by Django 2.0.6 on 2020-01-27 09:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0009_auto_20191009_0905'),
]
operations = [
migrations.CreateModel(
name='MagentoToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(blank=True, max_length=64, null=True)),
('created_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_token', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2020-01-27 13:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0010_magentotoken'),
]
operations = [
migrations.AddField(
model_name='user',
name='hep_id',
field=models.PositiveIntegerField(null=True),
),
]

View File

@ -15,6 +15,7 @@ class User(AbstractUser):
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
avatar_url = models.CharField(max_length=254, blank=True, default='')
email = models.EmailField(_('email address'), unique=True)
hep_id = models.PositiveIntegerField(null=True, blank=False)
objects = UserManager()
@ -166,3 +167,8 @@ class UserSetting(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting')
selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)
class MagentoToken(models.Model):
token = models.CharField(blank=True, null=True, max_length=64)
created_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, blank=False, null=False, on_delete=models.CASCADE, related_name='user_token')

View File

@ -15,6 +15,8 @@ from graphene import relay
from core.hep_client import HepClient
from registration.models import License
from users.managers import UserRoleManager
from users.models import MagentoToken, User, Role, UserRole, SchoolClass
class LoginError(graphene.ObjectType):
@ -32,6 +34,9 @@ class Login(relay.ClientIDMutation):
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
username = kwargs.get('username_input')
password = kwargs.get('password_input')
# get token
# wrong password
#
@ -44,8 +49,8 @@ class Login(relay.ClientIDMutation):
# save role information
# login
if not settings.USE_LOCAL_REGISTRATION:
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
if settings.USE_LOCAL_REGISTRATION:
user = authenticate(username=username, password=password)
if user is None:
error = LoginError(field='invalid_credentials')
return cls(success=False, errors=[error])
@ -53,24 +58,72 @@ class Login(relay.ClientIDMutation):
else:
hep_client = HepClient()
# Todo catch error
token = hep_client.customer_token(kwargs.get('username_input'), kwargs.get('password_input'))
# Todo save token
#verify email
# Todo network error catch error
token = hep_client.customer_token(username, password)
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
user = User.objects.create_user_from_hep(token)
# ISBN "123-4-5678-9012-3"
magento_token, created = MagentoToken.objects.get_or_create(user=user)
magento_token.token = token['token']
magento_token.save()
if not hep_client.is_email_verified(username):
# Todo handle unverifed emails
pass
try:
license = License.objects.get(licensee=user)
except License.DoesNotExist:
product = hep_client.myskillbox_product_for_customer(settings.HEP_ADMIN_TOKEN, user.hep_id)
if product:
license = License.objects.create_license_for_role(user, product['activated'],
product['raw'], product['edition'])
else:
# todo go to shop
pass
UserRole.objects.create_role_for_user(user, license.for_role.key)
default_class_name = SchoolClass.generate_default_group_name()
default_class = SchoolClass.objects.create(name=default_class_name)
user.school_classes.add(default_class)
# if teacher create class
# if student add to class if exists???
# no orders
# network errors
# Todo get orders from magento
# check items
# show buy or create license
user_license = None
# check items
# show buy page
try:
user_license = License.objects.get(licensee=user)
except License.DoesNotExist:
# current users have no license, allow them to login
pass
if not license.is_valid():
pass
if user_license is not None and not user_license.license_type.active:
error = LoginError(field='license_inactive')
return cls(success=False, errors=[error])
# show page
#
# user_license = None
#
# try:
# user_license = License.objects.get(licensee=user)
# except License.DoesNotExist:
# # current users have no license, allow them to login
# pass
#
# if user_license is not None and not user_license.license_type.active:
# error = LoginError(field='license_inactive')
# return cls(success=False, errors=[error])
login(info.context, user)
return cls(success=True, errors=[])

View File

@ -0,0 +1,41 @@
{
"id": 49124,
"group_id": 1,
"default_billing": "47579",
"default_shipping": "47579",
"confirmation": "41b58ba6598a618095e8c70625d7f052",
"created_at": "2018-07-19 15:05:27",
"updated_at": "2019-11-26 17:04:29",
"created_in": "hep verlag",
"email": "1heptest19072018@mailinator.com",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"gender": 2,
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 47579,
"customer_id": 49124,
"region": {
"region_code": null,
"region": null,
"region_id": 0
},
"region_id": 0,
"country_id": "CH",
"street": [
"Test"
],
"telephone": "",
"postcode": "0000",
"city": "Test",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"default_shipping": true,
"default_billing": true
}
]
}

View File

@ -8,30 +8,42 @@
# Created on 2019-10-02
# @author: chrigu <christian.cueni@iterativ.ch>
import json
import os
from datetime import timedelta
from unittest.mock import patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from django.utils import timezone
from graphene.test import Client
from api.schema_public import schema
from core.factories import UserFactory
from core.hep_client import HepClient
from registration.factories import LicenseTypeFactory, LicenseFactory
from users.models import Role
from registration.factories import LicenseFactory
from registration.models import License
from users.models import Role, MagentoToken, User
FAKE_TOKEN = 'abcd12345!'
with open('orders.json', 'r') as file:
dir_path = os.path.dirname(os.path.realpath(__file__))
with open('{}/valid_teacher_orders.json'.format(dir_path), 'r') as file:
order_data = file.read()
ORDERS = json.loads(order_data)
with open('{}/me_data.json'.format(dir_path), 'r') as file:
me_data = file.read()
ME_DATA = json.loads(me_data)
class PasswordResetTests(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch')
self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role")
self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role)
Role.objects.create_default_roles()
self.teacher_role = Role.objects.get_default_teacher_role()
request = RequestFactory().post('/')
@ -61,38 +73,77 @@ class PasswordResetTests(TestCase):
})
@patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN})
@patch.object(HepClient, 'customer_orders', return_value=ORDERS)
def test_user_can_login(self, orders_mock, token_mock):
def test_user_can_login_with_local_user_and_valid_local_license(self, token_mock):
now = timezone.now()
expiry_date = now + timedelta(365)
LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save()
result = self.make_login_mutation(self.user.email, 'test123')
token = MagentoToken.objects.get(user=self.user)
self.assertEqual(token.token, FAKE_TOKEN)
self.assertTrue(result.get('data').get('login').get('success'))
self.assertTrue(self.user.is_authenticated)
def test_user_cannot_login_with_invalid_password(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
@patch.object(HepClient, 'customer_token', return_value={'token': FAKE_TOKEN})
@patch.object(HepClient, '_customer_orders', return_value=ORDERS)
@patch.object(HepClient, 'customer_me', return_value=ME_DATA)
def test_user_can_login_with_local_user_and_remote_license(self, order_mock, token_mock, me_token):
result = self.make_login_mutation(ME_DATA['email'], 'test123')
result = self.make_login_mutation(self.user.email, 'test1234')
self.assertFalse(result.get('data').get('login').get('success'))
user = User.objects.get(email=ME_DATA['email'])
token = MagentoToken.objects.get(user=user)
self.assertEqual(token.token, FAKE_TOKEN)
def test_user_with_active_license_can_login(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
result = self.make_login_mutation(self.user.email, password)
self.assertTrue(result.get('data').get('login').get('success'))
self.assertTrue(self.user.is_authenticated)
def test_user_with_inactive_license_cannot_login(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
## can login with license and user
# can login with no user and license
# can login with no user and local license
# cannot login without user
# cannot login with user and not verfied
# cannot login with user and no license
# cannot login with user and expired license
# non 200 error
# if more than one valid license take correct
# if mulitple licenses and one correct take one test in own class
self.teacher_license_type.active = False
self.teacher_license_type.save()
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
result = self.make_login_mutation(self.user.email, password)
self.assertFalse(result.get('data').get('login').get('success'))
# def test_user_cannot_login_with_invalid_password(self):
# password = 'test123'
# self.user.set_password(password)
# self.user.save()
#
# result = self.make_login_mutation(self.user.email, 'test1234')
# self.assertFalse(result.get('data').get('login').get('success'))
#
# def test_user_with_active_license_can_login(self):
# password = 'test123'
# self.user.set_password(password)
# self.user.save()
#
# LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
#
# result = self.make_login_mutation(self.user.email, password)
# self.assertTrue(result.get('data').get('login').get('success'))
#
# def test_user_with_inactive_license_cannot_login(self):
# password = 'test123'
# self.user.set_password(password)
# self.user.save()
#
# self.teacher_license_type.active = False
# self.teacher_license_type.save()
# LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
#
# result = self.make_login_mutation(self.user.email, password)
# self.assertFalse(result.get('data').get('login').get('success'))

View File

@ -82,7 +82,7 @@
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)",
"name": "Myskillbox Lehreredition",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
@ -99,7 +99,7 @@
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1082-9",
"sku": "000-4-5678-9012-3",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,