diff --git a/Pipfile.lock b/Pipfile.lock index 616240ea..a6675b6f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -408,10 +408,10 @@ }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", + "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" ], - "version": "==2.4.0" + "version": "==2.4.1" }, "six": { "hashes": [ diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 84311ce4..8f15eb62 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -745,6 +745,12 @@ class RepositoryExamplesViewSet( ] +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True + ), +) class RegisterUserViewSet( mixins.CreateModelMixin, GenericViewSet): @@ -755,6 +761,13 @@ class RegisterUserViewSet( serializer_class = RegisterUserSerializer +@method_decorator( + name='create', + decorator=swagger_auto_schema( + responses={201: '{"token":"TOKEN"}'}, + deprecated=True + ), +) class LoginViewSet(GenericViewSet): """ @@ -778,6 +791,12 @@ def create(self, request, *args, **kwargs): status.HTTP_201_CREATED if created else status.HTTP_200_OK) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True + ), +) class ChangePasswordViewSet(GenericViewSet): """ Change current user password. @@ -812,6 +831,12 @@ def update(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST) +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True + ), +) class RequestResetPassword(GenericViewSet): """ Request reset password @@ -833,6 +858,12 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True + ), +) class ResetPassword(GenericViewSet): """ Reset password @@ -853,6 +884,24 @@ def update(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True + ), +) +@method_decorator( + name='retrieve', + decorator=swagger_auto_schema( + deprecated=True + ), +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + deprecated=True + ), +) class MyUserProfileViewSet( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, @@ -886,6 +935,12 @@ def get_object(self, *args, **kwargs): return user +@method_decorator( + name='retrieve', + decorator=swagger_auto_schema( + deprecated=True + ), +) class UserProfileViewSet( mixins.RetrieveModelMixin, GenericViewSet): @@ -1033,6 +1088,12 @@ def update(self, *args, **kwargs): return response +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class SearchUserViewSet( mixins.ListModelMixin, GenericViewSet): diff --git a/bothub/api/v2/account/__init__.py b/bothub/api/v2/account/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/account/serializers.py b/bothub/api/v2/account/serializers.py new file mode 100644 index 00000000..8b458ce6 --- /dev/null +++ b/bothub/api/v2/account/serializers.py @@ -0,0 +1,133 @@ +from django.utils.translation import gettext as _ +from django.contrib.auth.password_validation import validate_password +from django.contrib.auth.hashers import make_password +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from bothub.authentication.models import User +from ..fields import PasswordField + + +class LoginSerializer(AuthTokenSerializer, serializers.ModelSerializer): + username = serializers.EmailField( + label=_('Email'), + ) + password = PasswordField( + label=_('Password'), + ) + + class Meta: + model = User + fields = [ + 'username', + 'password', + ] + ref_name = None + + +class RegisterUserSerializer(serializers.ModelSerializer): + password = PasswordField( + write_only=True, + validators=[ + validate_password, + ]) + + class Meta: + model = User + fields = [ + 'email', + 'name', + 'nickname', + 'password', + ] + ref_name = None + + @staticmethod + def validate_password(value): + return make_password(value) + + +class ChangePasswordSerializer(serializers.ModelSerializer): + current_password = PasswordField( + required=True + ) + password = PasswordField( + required=True, + validators=[ + validate_password, + ] + ) + + class Meta: + model = User + fields = [ + 'current_password', + 'password' + ] + ref_name = None + + def validate_current_password(self, value): + request = self.context.get('request') + if not request.user.check_password(value): + raise ValidationError(_('Wrong password')) + return value + + +class RequestResetPasswordSerializer(serializers.ModelSerializer): + email = serializers.EmailField( + label=_('Email'), + required=True + ) + + class Meta: + model = User + fields = [ + 'email' + ] + ref_name = None + + def validate_email(self, value): + try: + User.objects.get(email=value) + return value + except User.DoesNotExist: + raise ValidationError(_('No user registered with this email')) + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'nickname', + 'name', + 'locale', + ] + ref_name = None + + +class ResetPasswordSerializer(serializers.ModelSerializer): + token = serializers.CharField( + label=_('Token'), + style={'show': False} + ) + password = PasswordField( + label=_('New Password'), + required=True, + validators=[ + validate_password, + ]) + + class Meta: + model = User + fields = [ + 'token', + 'password', + ] + ref_name = None + + def validate_token(self, value): + user = self.context.get('view').get_object() + if not user.check_password_reset_token(value): + raise ValidationError(_('Invalid token for this user')) + return value diff --git a/bothub/api/v2/account/views.py b/bothub/api/v2/account/views.py new file mode 100644 index 00000000..c79c3b18 --- /dev/null +++ b/bothub/api/v2/account/views.py @@ -0,0 +1,180 @@ +from django.utils.decorators import method_decorator +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter +from rest_framework.viewsets import GenericViewSet +from rest_framework.response import Response +from rest_framework.authtoken.models import Token +from rest_framework import status, mixins +from rest_framework import permissions +from drf_yasg.utils import swagger_auto_schema +from bothub.api.v2.metadata import Metadata +from bothub.authentication.models import User + +from .serializers import LoginSerializer +from .serializers import RegisterUserSerializer +from .serializers import ChangePasswordSerializer +from .serializers import RequestResetPasswordSerializer +from .serializers import UserSerializer +from .serializers import ResetPasswordSerializer + + +@method_decorator( + name='create', + decorator=swagger_auto_schema( + responses={201: '{"token":"TOKEN"}'} + ) +) +class LoginViewSet(mixins.CreateModelMixin, GenericViewSet): + + """ + Login Users + """ + + queryset = User.objects + serializer_class = LoginSerializer + lookup_field = ('username', 'password') + metadata_class = Metadata + + def create(self, request, *args, **kwargs): + serializer = self.serializer_class( + data=request.data, + context={'request': request}) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + token, created = Token.objects.get_or_create(user=user) + return Response( + { + 'token': token.key, + }, + status.HTTP_201_CREATED if created else status.HTTP_200_OK) + + +class RegisterUserViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Register new user + """ + queryset = User.objects + serializer_class = RegisterUserSerializer + lookup_field = ('email', 'name', 'nickname', 'password') + metadata_class = Metadata + + +class ChangePasswordViewSet(mixins.UpdateModelMixin, GenericViewSet): + """ + Change current user password. + """ + serializer_class = ChangePasswordSerializer + queryset = User.objects + lookup_field = None + permission_classes = [ + permissions.IsAuthenticated + ] + metadata_class = Metadata + + def get_object(self, *args, **kwargs): + request = self.request + user = request.user + + # May raise a permission denied + self.check_object_permissions(self.request, user) + + return user + + def update(self, request, *args, **kwargs): + self.object = self.get_object() + serializer = self.get_serializer(data=request.data) + + if serializer.is_valid(): + self.object.set_password(serializer.data.get('password')) + self.object.save() + return Response(status=status.HTTP_200_OK) + + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + + +class RequestResetPasswordViewSet(mixins.CreateModelMixin, GenericViewSet): + """ + Request reset password + """ + serializer_class = RequestResetPasswordSerializer + queryset = User.objects + lookup_field = ['email'] + permission_classes = [permissions.AllowAny] + metadata_class = Metadata + + def get_object(self): + return self.queryset.get(email=self.request.data.get('email')) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.object = self.get_object() + self.object.send_reset_password_email() + return Response(status=status.HTTP_201_CREATED) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + + +class UserProfileViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + GenericViewSet): + """ + Get user profile + """ + serializer_class = UserSerializer + queryset = User.objects + lookup_field = 'nickname' + permission_classes = [ + permissions.IsAuthenticatedOrReadOnly + ] + + +class SearchUserViewSet(mixins.ListModelMixin, GenericViewSet): + serializer_class = UserSerializer + queryset = User.objects.all() + filter_backends = [ + DjangoFilterBackend, + SearchFilter, + ] + search_fields = [ + '=name', + '^name', + '$name', + '=nickname', + '^nickname', + '$nickname', + '=email', + ] + pagination_class = None + limit = 5 + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset())[:self.limit] + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + +class ResetPasswordViewSet(mixins.UpdateModelMixin, GenericViewSet): + """ + Reset password + """ + serializer_class = ResetPasswordSerializer + queryset = User.objects + lookup_field = 'nickname' + + def update(self, request, *args, **kwargs): + self.object = self.get_object() + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.object.set_password(serializer.data.get('password')) + self.object.save() + return Response(status=status.HTTP_200_OK) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST) diff --git a/bothub/api/v2/evaluate/views.py b/bothub/api/v2/evaluate/views.py index 87e13e6d..6bb6b471 100644 --- a/bothub/api/v2/evaluate/views.py +++ b/bothub/api/v2/evaluate/views.py @@ -126,7 +126,6 @@ class EvaluateViewSet( """ Manager evaluate (tests). """ - lookup_fields = ('pk', 'repository_uuid') queryset = RepositoryEvaluate.objects serializer_class = RepositoryEvaluateSerializer permission_classes = [ @@ -196,7 +195,6 @@ class ResultsListViewSet( GenericViewSet): queryset = RepositoryEvaluateResult.objects - lookup_fields = ['repository_uuid'] serializer_class = RepositoryEvaluateResultVersionsSerializer permission_classes = [ IsAuthenticated, diff --git a/bothub/api/v2/metadata.py b/bothub/api/v2/metadata.py index 0677bc7e..6a0f707c 100644 --- a/bothub/api/v2/metadata.py +++ b/bothub/api/v2/metadata.py @@ -5,6 +5,11 @@ from rest_framework import serializers from rest_framework.utils.field_mapping import ClassLookupDict +from bothub.api.v2.fields import PasswordField +from bothub.api.v2.fields import ModelMultipleChoiceField +from bothub.api.v2.fields import TextField +from bothub.api.v2.fields import EntityText + class Metadata(BaseMetadata): label_lookup = ClassLookupDict({ @@ -30,6 +35,11 @@ class Metadata(BaseMetadata): serializers.DictField: 'nested object', serializers.Serializer: 'nested object', serializers.ManyRelatedField: 'multiple choice', + serializers.HiddenField: 'hidden', + PasswordField: 'password', + ModelMultipleChoiceField: 'multiple choice', + TextField: 'text', + EntityText: 'entity text', }) def determine_metadata(self, request, view): @@ -52,7 +62,7 @@ def determine_metadata(self, request, view): def determine_actions(self, request, view): actions = {} - for method in ['PUT', 'POST']: + for method in {'PUT', 'POST'} & set(view.allowed_methods): serializer = view.get_serializer() actions[method] = self.get_serializer_info(serializer) view.request = request diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index c2d90f91..f4379cdc 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -70,7 +70,7 @@ class RepositoryVotesViewSet( """ queryset = RepositoryVote.objects.all() lookup_field = 'repository' - lookup_fields = ['user', 'repository'] + lookup_fields = ['repository'] serializer_class = RepositoryVotesSerializer permission_classes = [ IsAuthenticatedOrReadOnly diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 680ee2c7..80147f87 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -7,9 +7,85 @@ from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet +from .account.views import LoginViewSet +from .account.views import RegisterUserViewSet +from .account.views import ChangePasswordViewSet +from .account.views import RequestResetPasswordViewSet +from .account.views import UserProfileViewSet +from .account.views import SearchUserViewSet +from .account.views import ResetPasswordViewSet -router = routers.SimpleRouter() +class Router(routers.SimpleRouter): + routes = [ + # Dynamically generated list routes. + # Generated using @action decorator + # on methods of the viewset. + routers.DynamicRoute( + url=r'^{prefix}/{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=True, + initkwargs={}, + ), + # Dynamically generated detail routes. + # Generated using @action decorator on methods of the viewset. + routers.DynamicRoute( + url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=True, + initkwargs={}, + ), + ] + + def get_routes(self, viewset): + ret = super().get_routes(viewset) + lookup_field = getattr(viewset, 'lookup_field', None) + + if lookup_field: + # List route. + ret.append(routers.Route( + url=r'^{prefix}{trailing_slash}$', + mapping={ + 'get': 'list', + 'post': 'create' + }, + name='{basename}-list', + detail=False, + initkwargs={'suffix': 'List'}, + )) + + detail_url_regex = r'^{prefix}/{lookup}{trailing_slash}$' + if not lookup_field: + detail_url_regex = r'^{prefix}{trailing_slash}$' + # Detail route. + ret.append(routers.Route( + url=detail_url_regex, + mapping={ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }, + name='{basename}-detail', + detail=True, + initkwargs={'suffix': 'Instance'} + )) + + return ret + + def get_lookup_regex(self, viewset, lookup_prefix=''): + lookup_fields = getattr(viewset, 'lookup_fields', None) + if lookup_fields: + base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>[^/.]+)' + return '/'.join(map( + lambda x: base_regex.format( + lookup_prefix=lookup_prefix, + lookup_url_kwarg=x), + lookup_fields)) + return super().get_lookup_regex(viewset, lookup_prefix) + + +router = Router() router.register('repository', RepositoryViewSet) router.register('repository-votes', RepositoryVotesViewSet) router.register('repositories', RepositoriesViewSet) @@ -17,3 +93,10 @@ router.register('examples', ExamplesViewSet) router.register('evaluate/results', ResultsListViewSet) router.register('evaluate', EvaluateViewSet) +router.register('account/login', LoginViewSet) +router.register('account/register', RegisterUserViewSet) +router.register('account/change-password', ChangePasswordViewSet) +router.register('account/forgot-password', RequestResetPasswordViewSet) +router.register('account/user-profile', UserProfileViewSet) +router.register('account/search-user', SearchUserViewSet) +router.register('account/reset-password', ResetPasswordViewSet) diff --git a/bothub/api/v2/tests/test_account.py b/bothub/api/v2/tests/test_account.py new file mode 100644 index 00000000..aabfa3fe --- /dev/null +++ b/bothub/api/v2/tests/test_account.py @@ -0,0 +1,359 @@ +import json + +from django.test import TestCase +from django.test import RequestFactory +from django.test.client import MULTIPART_CONTENT +from rest_framework import status + +from bothub.authentication.models import User + +from ..account.views import RegisterUserViewSet +from ..account.views import LoginViewSet +from ..account.views import ChangePasswordViewSet +from ..account.views import RequestResetPasswordViewSet +from ..account.views import ResetPasswordViewSet +from ..account.views import UserProfileViewSet + +from .utils import create_user_and_token + + +class LoginTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.password = 'abcgq!!123' + self.email = 'user@user.com' + + user = User.objects.create( + email=self.email, + nickname='user', + name='User') + user.set_password(self.password) + user.save(update_fields=['password']) + + def request(self, data): + request = self.factory.post( + '/v2/account/login/', + data) + response = LoginViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'username': self.email, + 'password': self.password, + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertIn( + 'token', + content_data.keys()) + + def test_wrong_password(self): + response, content_data = self.request({ + 'username': self.email, + 'password': 'wrong', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + +class RegisterUserTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + def request(self, data): + request = self.factory.post( + '/v2/account/register/', + data) + response = RegisterUserViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + email = 'fake@user.com' + password = 'abc!1234' + response, content_data = self.request({ + 'email': email, + 'name': 'Fake', + 'nickname': 'fake', + 'password': password, + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + user = User.objects.get(email=email) + self.assertTrue(user.check_password(password)) + + def test_invalid_password(self): + response, content_data = self.request({ + 'email': 'fake@user.com', + 'name': 'Fake', + 'nickname': 'fake', + 'password': 'abc', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'password', + content_data.keys()) + + def test_unique_nickname(self): + nickname = 'fake' + User.objects.create_user('user1@user.com', nickname) + response, content_data = self.request({ + 'email': 'user2@user.com', + 'name': 'Fake', + 'nickname': nickname, + 'password': 'abc!1234', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'nickname', + content_data.keys()) + + def test_invalid_nickname_url_conflict(self): + URL_PATHS = [ + 'api', + 'docs', + 'admin', + ] + + for url_path in URL_PATHS: + response, content_data = self.request({ + 'email': '{}@fake.com'.format(url_path), + 'name': 'Fake', + 'nickname': url_path, + 'password': 'abc!1234', + }) + + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'nickname', + content_data.keys()) + + +class RequestResetPasswordTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.email = 'user@user.com' + + User.objects.create( + email=self.email, + nickname='user', + name='User') + + def request(self, data): + request = self.factory.post( + '/v2/account/forgot-password/', + data) + response = RequestResetPasswordViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content or 'null') + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'email': self.email, + }) + + def test_email_not_found(self): + response, content_data = self.request({ + 'email': 'nouser@fake.com', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'email', + content_data.keys()) + + +class ResetPasswordTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.user = User.objects.create( + email='user@user.com', + nickname='user', + name='User') + self.reset_password_token = self.user.make_password_reset_token() + + def request(self, nickname, data): + request = self.factory.post( + '/v2/account/reset-password/{}/'.format(nickname), + data) + response = ResetPasswordViewSet.as_view( + {'post': 'update'})(request, nickname=nickname) + response.render() + content_data = json.loads(response.content or 'null') + return (response, content_data,) + + def test_okay(self): + new_password = 'valid12!' + response, content_data = self.request( + self.user.nickname, + { + 'token': self.reset_password_token, + 'password': new_password, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.user = User.objects.get(pk=self.user.pk) + self.assertTrue(self.user.check_password(new_password)) + + def test_invalid_token(self): + response, content_data = self.request( + self.user.nickname, + { + 'token': '112233', + 'password': 'valid12!', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'token', + content_data.keys()) + + +class ChangePasswordTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.user, self.user_token = create_user_and_token() + self.password = '12555q!66' + self.user.set_password(self.password) + self.user.save(update_fields=['password']) + + def request(self, data, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.post( + '/v2/account/change-password/', + data, + **authorization_header) + response = ChangePasswordViewSet.as_view( + {'post': 'update'})(request) + response.render() + content_data = json.loads(response.content or 'null') + return (response, content_data,) + + def test_okay(self): + new_password = 'kkl8&!qq' + response, content_data = self.request( + { + 'current_password': self.password, + 'password': new_password, + }, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + + def test_wrong_password(self): + response, content_data = self.request( + { + 'current_password': 'wrong_password', + 'password': 'new_password', + }, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'current_password', + content_data.keys()) + + +class ListUserProfileTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.user, self.user_token = create_user_and_token() + + def request(self, token, nickname): + request = self.factory.get( + '/v2/account/user-profile/{}/'.format(nickname) + ) + response = UserProfileViewSet.as_view( + {'get': 'retrieve'})(request, nickname=nickname) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.user_token, + self.user.nickname + ) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('nickname'), + self.user.nickname) + + def test_not_exists(self): + response, content_data = self.request(self.user_token, 'no_exists') + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND) + self.assertEqual( + content_data.get('detail'), + 'Not found.') + + +class UserUpdateTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.user, self.user_token = create_user_and_token() + + def request(self, user, data, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.patch( + '/v2/account/user-profile/{}/'.format(self.user.nickname), + self.factory._encode_data(data, MULTIPART_CONTENT), + MULTIPART_CONTENT, + **authorization_header) + response = UserProfileViewSet.as_view( + {'patch': 'update'})( + request, + pk=user.pk, + nickname=user.nickname, + partial=True + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + new_locale = 'MaceiĆ³ - Alagoas' + response, content_data = self.request( + self.user, + { + 'locale': new_locale, + }, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('locale'), + new_locale)