feat: added users app
This commit is contained in:
@@ -10,4 +10,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||||
path("shop/", include("shop.api.v1.routers")),
|
path("shop/", include("shop.api.v1.routers")),
|
||||||
|
path("files/", include("files.api.v1.urls")),
|
||||||
|
path("auth/", include("users.api.v1.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ THIRD_PARTY_APPS = [
|
|||||||
"django_filters",
|
"django_filters",
|
||||||
"watchman",
|
"watchman",
|
||||||
"drf_spectacular",
|
"drf_spectacular",
|
||||||
|
"rest_framework_simplejwt",
|
||||||
]
|
]
|
||||||
|
|
||||||
PROJECT_APPS = [
|
PROJECT_APPS = [
|
||||||
|
"files",
|
||||||
"frontend",
|
"frontend",
|
||||||
"shop",
|
"shop",
|
||||||
"tpv",
|
"tpv",
|
||||||
@@ -179,3 +181,43 @@ SPECTACULAR_SETTINGS = {
|
|||||||
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0")
|
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0")
|
||||||
CELERY_TIME_ZONE = TIME_ZONE
|
CELERY_TIME_ZONE = TIME_ZONE
|
||||||
CELERY_ALWAYS_EAGER = DEBUG
|
CELERY_ALWAYS_EAGER = DEBUG
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(minutes=5),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=1),
|
||||||
|
"ROTATE_REFRESH_TOKENS": False,
|
||||||
|
"BLACKLIST_AFTER_ROTATION": False,
|
||||||
|
"UPDATE_LAST_LOGIN": False,
|
||||||
|
|
||||||
|
"ALGORITHM": "HS256",
|
||||||
|
"SIGNING_KEY": SECRET_KEY,
|
||||||
|
"VERIFYING_KEY": "",
|
||||||
|
"AUDIENCE": None,
|
||||||
|
"ISSUER": None,
|
||||||
|
"JSON_ENCODER": None,
|
||||||
|
"JWK_URL": None,
|
||||||
|
"LEEWAY": 0,
|
||||||
|
|
||||||
|
"AUTH_HEADER_TYPES": ("Bearer", "JWT", ),
|
||||||
|
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||||
|
"USER_ID_FIELD": "id",
|
||||||
|
"USER_ID_CLAIM": "user_id",
|
||||||
|
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
|
||||||
|
|
||||||
|
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
|
||||||
|
"TOKEN_TYPE_CLAIM": "token_type",
|
||||||
|
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
|
||||||
|
|
||||||
|
"JTI_CLAIM": "jti",
|
||||||
|
|
||||||
|
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
|
||||||
|
"SLIDING_TOKEN_LIFETIME": datetime.timedelta(minutes=5),
|
||||||
|
"SLIDING_TOKEN_REFRESH_LIFETIME": datetime.timedelta(days=1),
|
||||||
|
|
||||||
|
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
|
||||||
|
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
|
||||||
|
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
|
||||||
|
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
|
||||||
|
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
|
||||||
|
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import debug_toolbar
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
@@ -9,3 +12,7 @@ urlpatterns = [
|
|||||||
path("tpv/", include("tpv.urls", namespace="tpv")),
|
path("tpv/", include("tpv.urls", namespace="tpv")),
|
||||||
path("auth/", include("users.urls", namespace="users")),
|
path("auth/", include("users.urls", namespace="users")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from files.models import FileUpload
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FileUpload
|
||||||
|
fields = ('file', 'id')
|
||||||
|
read_only_fields = ('id', )
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from files.api.v1.views import FileUploadViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter(trailing_slash=True)
|
||||||
|
router.register('', FileUploadViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from files.api.v1.serializers import FileUploadSerializer
|
||||||
|
from files.models import FileUpload
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadViewSet(ModelViewSet):
|
||||||
|
serializer_class = FileUploadSerializer
|
||||||
|
queryset = FileUpload.objects.all()
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FilesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "files"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-05-04 22:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FileUpload",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("file", models.FileField(upload_to="uploads")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class FileUpload(models.Model):
|
||||||
|
file = models.FileField(upload_to="uploads", blank=False, null=False)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -2,6 +2,7 @@ celery==5.2.7
|
|||||||
django==5.0.3
|
django==5.0.3
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
django-cryptography==1.1
|
django-cryptography==1.1
|
||||||
|
django-debug-toolbar==3.7.0
|
||||||
django-extensions==3.2.0
|
django-extensions==3.2.0
|
||||||
django-filter==23.1
|
django-filter==23.1
|
||||||
django-environ==0.10.0
|
django-environ==0.10.0
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,6 @@ class ProductAdmin(ModelAdmin):
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"stock",
|
"stock",
|
||||||
"unit",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +23,7 @@ class ProductPriceAdmin(ModelAdmin):
|
|||||||
"product",
|
"product",
|
||||||
"price",
|
"price",
|
||||||
"date",
|
"date",
|
||||||
|
"tax",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+26
-11
@@ -1,25 +1,49 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from files.api.v1.serializers import FileUploadSerializer
|
||||||
from shop.models import Product, ProductPrice, Customer, Tax, OrderLine, Order
|
from shop.models import Product, ProductPrice, Customer, Tax, OrderLine, Order
|
||||||
|
|
||||||
|
|
||||||
class ProductSerializer(serializers.ModelSerializer):
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
|
images = FileUploadSerializer(many=True, read_only=True)
|
||||||
|
price = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_price(self, product):
|
||||||
|
last_price = product.prices.last()
|
||||||
|
|
||||||
|
return ProductPriceSerializer(last_price).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = (
|
fields = (
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"stock",
|
"stock",
|
||||||
"unit",
|
"is_digital_asset",
|
||||||
|
"images",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaxSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Tax
|
||||||
|
fields = (
|
||||||
|
"code",
|
||||||
|
"value",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProductPriceSerializer(serializers.ModelSerializer):
|
class ProductPriceSerializer(serializers.ModelSerializer):
|
||||||
|
tax = TaxSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProductPrice
|
model = ProductPrice
|
||||||
fields = (
|
fields = (
|
||||||
"price",
|
"price",
|
||||||
"date",
|
"date",
|
||||||
"product",
|
"tax",
|
||||||
|
"price_with_tax",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,15 +63,6 @@ class CustomerSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaxSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Tax
|
|
||||||
fields = (
|
|
||||||
"code",
|
|
||||||
"value",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrderSerializer(serializers.ModelSerializer):
|
class OrderSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-05-04 22:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("files", "0001_initial"),
|
||||||
|
("shop", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="product",
|
||||||
|
name="unit",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="order",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(default="PEN", max_length=3, verbose_name="Estado"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="product",
|
||||||
|
name="images",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="files.fileupload", verbose_name="Imágenes"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="product",
|
||||||
|
name="is_digital_asset",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="product",
|
||||||
|
name="url",
|
||||||
|
field=models.URLField(blank=True, verbose_name="URL de descarga"),
|
||||||
|
),
|
||||||
|
]
|
||||||
+5
-2
@@ -26,7 +26,10 @@ class Product(models.Model):
|
|||||||
verbose_name=_("Stock"),
|
verbose_name=_("Stock"),
|
||||||
)
|
)
|
||||||
is_digital_asset = models.BooleanField(default=False)
|
is_digital_asset = models.BooleanField(default=False)
|
||||||
url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
|
url = models.URLField(blank=True, verbose_name=_("URL de descarga"))
|
||||||
|
images = models.ManyToManyField(
|
||||||
|
"files.FileUpload", blank=True, verbose_name=_("Imágenes")
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -58,7 +61,7 @@ class ProductPrice(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def price_with_tax(self):
|
def price_with_tax(self):
|
||||||
tax_value = self.price * (self.tax.value / 100)
|
tax_value = self.price * Decimal(self.tax.value / 100)
|
||||||
return round(self.price + tax_value, 2)
|
return round(self.price + tax_value, 2)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -16,17 +16,14 @@ class ShopModelsTest(TestCase):
|
|||||||
self.potatoes = Product.objects.create(
|
self.potatoes = Product.objects.create(
|
||||||
name="Patatas",
|
name="Patatas",
|
||||||
stock=Decimal("100.00"),
|
stock=Decimal("100.00"),
|
||||||
unit=Product.UnitChoices.WEIGHT_KG,
|
|
||||||
)
|
)
|
||||||
self.gasoline = Product.objects.create(
|
self.gasoline = Product.objects.create(
|
||||||
name="Gasolina",
|
name="Gasolina",
|
||||||
stock=Decimal("800.00"),
|
stock=Decimal("800.00"),
|
||||||
unit=Product.UnitChoices.VOLUME_LITER,
|
|
||||||
)
|
)
|
||||||
self.usb_c = Product.objects.create(
|
self.usb_c = Product.objects.create(
|
||||||
name="Cable USB-C",
|
name="Cable USB-C",
|
||||||
stock=Decimal("5.00"),
|
stock=Decimal("5.00"),
|
||||||
unit=Product.UnitChoices.UNIT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ProductPrice.objects.create(
|
ProductPrice.objects.create(
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.password_validation import get_password_validators, validate_password
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = getattr(settings, 'AUTH_PASSWORD_VALIDATORS')
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
permissions = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_permissions(self, obj: User) -> List[str]:
|
||||||
|
group_permissions = obj.get_group_permissions()
|
||||||
|
user_permissions = obj.get_user_permissions()
|
||||||
|
|
||||||
|
return sorted(user_permissions.union(group_permissions))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser',
|
||||||
|
'permissions',
|
||||||
|
)
|
||||||
|
read_only_fields = ('username', 'code', 'is_staff', 'is_superuser', 'permissions', )
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePasswordSerializer(serializers.ModelSerializer):
|
||||||
|
old_password = serializers.CharField(write_only=True)
|
||||||
|
new_password = serializers.CharField(write_only=True)
|
||||||
|
new_password2 = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
def validate_old_password(self, password):
|
||||||
|
user = self.context.get('request').user
|
||||||
|
|
||||||
|
if not user.check_password(password):
|
||||||
|
raise ValidationError('Wrong old password')
|
||||||
|
|
||||||
|
return password
|
||||||
|
|
||||||
|
def validate_new_password(self, password):
|
||||||
|
old_password = self.context.get('request').data.get('old_password')
|
||||||
|
|
||||||
|
if password == old_password:
|
||||||
|
raise ValidationError('Password can\'t be the same as the old one')
|
||||||
|
|
||||||
|
password2 = self.context.get('request').data.get('new_password2')
|
||||||
|
if password != password2:
|
||||||
|
raise ValidationError('Password mismatch')
|
||||||
|
|
||||||
|
validate_password(password, password_validators=get_password_validators(AUTH_PASSWORD_VALIDATORS))
|
||||||
|
|
||||||
|
return password
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
new_password = self.validated_data.get('new_password')
|
||||||
|
self.instance.set_password(new_password)
|
||||||
|
self.instance.save()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('old_password', 'new_password', 'new_password2', )
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from users.api.v1.views import retrieve_update_me, change_password
|
||||||
|
|
||||||
|
from rest_framework_simplejwt.views import (
|
||||||
|
token_refresh,
|
||||||
|
token_obtain_pair,
|
||||||
|
token_verify
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('login/', token_obtain_pair, name='login'),
|
||||||
|
path('refresh/', token_refresh, name='refresh_jwt'),
|
||||||
|
path('verify/', token_verify, name='verify_jwt'),
|
||||||
|
path('me/', retrieve_update_me, name='user_info'),
|
||||||
|
path('change-password/', change_password, name='change_password'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.generics import RetrieveUpdateAPIView, UpdateAPIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from users.api.v1.serializers import UserSerializer, UpdatePasswordSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RetrieveUpdateMe(RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
get: Return user info
|
||||||
|
|
||||||
|
put: Update user info
|
||||||
|
"""
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePassword(UpdateAPIView):
|
||||||
|
"""
|
||||||
|
put: Update user's password
|
||||||
|
"""
|
||||||
|
serializer_class = UpdatePasswordSerializer
|
||||||
|
permission_classes = (IsAuthenticated, )
|
||||||
|
queryset = User.objects.all()
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
retrieve_update_me = RetrieveUpdateMe.as_view()
|
||||||
|
change_password = ChangePassword.as_view()
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class TestChangePassword(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.password = 'theonering'
|
||||||
|
self.user = User.objects.create(
|
||||||
|
username='sauron',
|
||||||
|
email='sauron@mordor.middleearth',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user.set_password(self.password)
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
def test_login_then_change_password(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||||
|
response = self.client.put('/api/v1/auth/change-password/', {
|
||||||
|
'new_password': 'barad-dur',
|
||||||
|
'new_password2': 'barad-dur',
|
||||||
|
'old_password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_login_then_change_password_with_wrong_password(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||||
|
response = self.client.put('/api/v1/auth/change-password/', {
|
||||||
|
'new_password': 'barad-dur',
|
||||||
|
'new_password2': 'barad-dur',
|
||||||
|
'old_password': 'incorrectoldpassword',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
def test_login_then_change_password_mismatch_password(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||||
|
response = self.client.put('/api/v1/auth/change-password/', {
|
||||||
|
'new_password': 'barad-dur',
|
||||||
|
'new_password2': 'mountdoom',
|
||||||
|
'old_password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
def test_login_then_change_password_but_its_the_same(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||||
|
response = self.client.put('/api/v1/auth/change-password/', {
|
||||||
|
'new_password': self.password,
|
||||||
|
'new_password2': self.password,
|
||||||
|
'old_password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLogin(APITestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.password = 'theonering'
|
||||||
|
self.user = User.objects.create(
|
||||||
|
username='sauron',
|
||||||
|
email='sauron@mordor.middleearth',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user.set_password(self.password)
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
def test_login(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.data.get('access') is not None
|
||||||
|
assert response.data.get('refresh') is not None
|
||||||
|
|
||||||
|
def test_login_then_verify(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
response = self.client.post('/api/v1/auth/verify/', {
|
||||||
|
'token': jwt_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_login_then_refresh(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('refresh')
|
||||||
|
response = self.client.post('/api/v1/auth/refresh/', {
|
||||||
|
'refresh': jwt_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.data.get('access') is not None
|
||||||
|
assert response.data.get('access') != jwt_token
|
||||||
|
|
||||||
|
def test_login_then_get_user_info(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
jwt_token = response.data.get('access')
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'JWT {jwt_token}')
|
||||||
|
response = self.client.get('/api/v1/auth/me/')
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.data.get('email') == self.user.email
|
||||||
|
|
||||||
|
def test_login_failed(self):
|
||||||
|
response = self.client.post('/api/v1/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': 'wrongpassword',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class TestUsers(TestCase):
|
||||||
|
|
||||||
|
def test_create_user(self):
|
||||||
|
User.objects.create_user({
|
||||||
|
'username': 'user 1',
|
||||||
|
'password': 'password1'
|
||||||
|
})
|
||||||
|
User.objects.all().count() == 1
|
||||||
|
|
||||||
|
def test_new_user_invalid_username(self):
|
||||||
|
"""Test creating user with no username raises error"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
User.objects.create_user(None, 'test123')
|
||||||
|
|
||||||
|
def test_create_new_superuser(self):
|
||||||
|
"""Test creating a new superuser"""
|
||||||
|
# Creation with standard method
|
||||||
|
user = User.objects.create_superuser(
|
||||||
|
'testsuperuser@adminemail.com',
|
||||||
|
'testadmin123'
|
||||||
|
)
|
||||||
|
assert user.is_superuser
|
||||||
|
assert user.is_staff
|
||||||
|
|
||||||
|
def test_create_group(self):
|
||||||
|
Group.objects.create(name='Group1')
|
||||||
|
assert Group.objects.count() == 1
|
||||||
Reference in New Issue
Block a user