diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index cdae7ac653fa10acd68b54b677b061c1ebf37b8e..ec71f10ef8c8909b1b439d7fe4b21286abe449fd 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -76,3 +76,15 @@ class AccountRegisterSerializer(serializers.ModelSerializer): def to_representation(self, instance): serializer = AccountSerializer(instance) return serializer.data + + +class AccountChangePassSerializer(serializers.ModelSerializer): + new_password = serializers.CharField(max_length=128) + confirm_new_password = serializers.CharField(max_length=128) + + class Meta: + model = Account + fields = [ + "new_password", + "confirm_new_password", + ] diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index c6accaa5f59fef2fcb84696e01998e5367881d6c..bee38d322746e528af855949deb673513c01d768 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -3,11 +3,10 @@ import random from faker import Faker from django.urls import reverse from django.core import mail -from django.core.exceptions import ValidationError from rest_framework import status from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase, APIClient -from unittest.mock import patch +from django.contrib.auth.models import User from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory from apps.accounts.models import Account, AccountHistory @@ -208,28 +207,6 @@ class AccountViewTest(APITestCase): response = self.client.post(path=url, data=data, format="json",) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - @patch('apps.accounts.models.Account.objects.create') - def test_create_new_account_fail_should_roll_back_user_creation(self, account_mock): - url = self.BASE_URL - _account_id = self.faker.email() - - data = { - "name": self.officer.name, - "username": _account_id, - "password": "justpass", - "email": _account_id, - "phone_number": "+999999999999", - "district": self.officer.district, - "sub_district": self.officer.sub_district, - "is_admin": False, - "is_verified": False, - "is_active": False, - } - - account_mock.side_effect = ValidationError('ValidationError raised!') - response = self.client.post(path=url, data=data, format="json",) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_create_new_account_fails_with_invalid_district_value(self): url = self.BASE_URL _account_id = self.faker.email() @@ -414,3 +391,35 @@ class AccountViewTest(APITestCase): response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_change_password_success(self): + url = self.BASE_URL + "change_password" + "/" + old_pass = "justpass" + new_pass = "changepass" + data = { + "new_password": new_pass, + "confirm_new_password": new_pass, + } + + response = self.client.put(path=url, data=data, format="json",) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.admin.user.check_password(old_pass), False) + + def test_change_password_fails_with_inconsistent_confirmation_password(self): + url = self.BASE_URL + "change_password" + "/" + data = { + "new_password": "changedjustpass", + "confirm_new_password": "inconsistent", + } + + response = self.client.put(path=url, data=data, format="json",) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_generate_random_password(self): + url = self.BASE_URL + "generate_random_password" + "/" + + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/accounts/tests/test_units/test_models.py b/apps/accounts/tests/test_units/test_models.py index 79f3da4708e7382ecf3f44df70667165ae7c0187..1650836555bebaade9bc3649648e7f82ba7cc3be 100644 --- a/apps/accounts/tests/test_units/test_models.py +++ b/apps/accounts/tests/test_units/test_models.py @@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError from ...models import Account, AccountHistory + class AccountModelTest(TestCase): def test_validate_inconsistent_district_data_should_raise_error(self): account = Account(district='Beji', sub_district='Limo') @@ -12,4 +13,4 @@ class AccountModelTest(TestCase): class AccountHistoryModelTest(TestCase): def test_validate_inconsistent_district_data_should_raise_error(self): account = AccountHistory(district='Beji', sub_district='Limo') - self.assertRaises(ValidationError, account.save) \ No newline at end of file + self.assertRaises(ValidationError, account.save) diff --git a/apps/accounts/tests/test_units/test_serializers.py b/apps/accounts/tests/test_units/test_serializers.py index 903b51a7091b82531f41b44064a523c8c9b883d3..d898d6e15a7272b1d382bbfa6aac09b210230ad6 100644 --- a/apps/accounts/tests/test_units/test_serializers.py +++ b/apps/accounts/tests/test_units/test_serializers.py @@ -1,7 +1,7 @@ from django.test.testcases import TestCase from rest_framework.exceptions import ValidationError -from ...serializers import AccountSerializer, AccountRegisterSerializer +from ...serializers import AccountSerializer, AccountRegisterSerializer, AccountChangePassSerializer from ..factories.accounts import AccountFactory @@ -52,3 +52,18 @@ class AccountRegisterSerializerTest(TestCase): AccountRegisterSerializer( data=data).is_valid, raise_exception=True) + +class AccountChangePassSerializerTest(TestCase): + def test_account_change_pass_inconsistent_should_raise_error( + self): + account = AccountFactory() + data = { + 'new_password': 'justpass', + 'confirm_new_passwored': 'exampleinconsistent', + } + + self.assertRaises( + ValidationError, + AccountChangePassSerializer( + data=data).is_valid, + raise_exception=True) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index c42086c63f0fefe853256ed63faebec80275d5de..5da6d5605192f1dfef988ddbe14e0cf21b7c658b 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,5 +1,6 @@ from django.core.mail import send_mail from django.contrib.auth.models import AnonymousUser, User +from django.contrib.auth.base_user import BaseUserManager from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status, viewsets @@ -9,6 +10,8 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response from django.core.exceptions import ValidationError from django.db import transaction +from random import SystemRandom +from string import ascii_uppercase, ascii_lowercase, digits from apps.accounts.filters import ( ACCOUNT_FILTERSET_FIELDS, @@ -19,6 +22,7 @@ from apps.accounts.models import Account from apps.accounts.serializers import ( AccountSerializer, AccountRegisterSerializer, + AccountChangePassSerializer, ) from apps.commons.permissions import ( IsAuthenticated, @@ -58,6 +62,8 @@ class AccountViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.action in ['create']: return AccountRegisterSerializer + if self.action in ['change_password', 'generate_random_password']: + return AccountChangePassSerializer return AccountSerializer def create(self, request): @@ -123,3 +129,37 @@ class AccountViewSet(viewsets.ModelViewSet): serializer = AccountSerializer(instance) return Response(serializer.data, status=status.HTTP_200_OK) + + @action(detail=False, methods=["put"], url_path="change_password") + def change_password(self, request): + user = request.user + username = user.username + serializer_class = self.get_serializer_class() + serializer = serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + new_password = serializer.validated_data.pop("new_password") + confirm_new_password = serializer.validated_data.pop("confirm_new_password") + + if new_password == confirm_new_password: + user = User.objects.get(username=username) + user.set_password(new_password) + user.save() + return Response("Password successfully changed", status=status.HTTP_200_OK) + return Response( + "Password and confirmation didn't match", + status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=["get"], url_path="generate_random_password") + def generate_random_password(self, request): + user = request.user + username = user.username + + new_password = ''.join(SystemRandom().choice(ascii_lowercase + ascii_uppercase + digits) + for _ in range(10)) + + user = User.objects.get(username=username) + user.set_password(new_password) + user.save() + return Response({"Success, the random password is: ", + new_password}, status=status.HTTP_200_OK)