From f6324a2101ace71e34efd3377001e24ff497f8fa Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 01:04:20 +0700 Subject: [PATCH 01/43] init google app --- apps/google/__init__.py | 0 apps/google/admin.py | 3 +++ apps/google/apps.py | 5 +++++ apps/google/migrations/__init__.py | 0 apps/google/models.py | 3 +++ apps/google/tests.py | 3 +++ apps/google/tests/__init__.py | 0 apps/google/tests/test_units/__init__.py | 0 apps/google/tests/test_units/test_google.py | 0 apps/google/views.py | 3 +++ project/settings.py | 1 + 11 files changed, 18 insertions(+) create mode 100644 apps/google/__init__.py create mode 100644 apps/google/admin.py create mode 100644 apps/google/apps.py create mode 100644 apps/google/migrations/__init__.py create mode 100644 apps/google/models.py create mode 100644 apps/google/tests.py create mode 100644 apps/google/tests/__init__.py create mode 100644 apps/google/tests/test_units/__init__.py create mode 100644 apps/google/tests/test_units/test_google.py create mode 100644 apps/google/views.py diff --git a/apps/google/__init__.py b/apps/google/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/admin.py b/apps/google/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/google/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/google/apps.py b/apps/google/apps.py new file mode 100644 index 0000000..a9146d3 --- /dev/null +++ b/apps/google/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GoogleConfig(AppConfig): + name = 'google' diff --git a/apps/google/migrations/__init__.py b/apps/google/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/models.py b/apps/google/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/google/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/google/tests.py b/apps/google/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/google/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/google/tests/__init__.py b/apps/google/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/tests/test_units/__init__.py b/apps/google/tests/test_units/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/views.py b/apps/google/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/google/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/project/settings.py b/project/settings.py index 7a219a3..b3b1219 100644 --- a/project/settings.py +++ b/project/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ "apps.exportables", "apps.logs", "apps.custom_auth", + "apps.google", ] MIDDLEWARE = [ -- GitLab From b12ede4e6f5414ec2f0e1ebb3115ebb7c36a1685 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 01:28:38 +0700 Subject: [PATCH 02/43] [CHORES] add skeleton --- apps/google/tests.py | 3 --- apps/google/tests/test_units/test_google.py | 22 +++++++++++++++++++++ apps/google/urls.py | 8 ++++++++ apps/google/views.py | 8 ++++++++ project/urls.py | 1 + 5 files changed, 39 insertions(+), 3 deletions(-) delete mode 100644 apps/google/tests.py create mode 100644 apps/google/urls.py diff --git a/apps/google/tests.py b/apps/google/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/apps/google/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index e69de29..c06db6c 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -0,0 +1,22 @@ +from rest_framework.test import APITestCase, APIClient +from rest_framework import status + +import json + + +class GoogleViewTest(APITestCase): + @classmethod + def setUpTestData(cls): + cls.BASE_URL = "/google/" + + def setUp(self): + self.noAuthClient = APIClient() + + def test_google_view(self): + url = self.BASE_URL + response = self.noAuthClient.post(url) + data = { + "message": "Google View!" + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertJSONEqual(json.dumps(response.data), data) diff --git a/apps/google/urls.py b/apps/google/urls.py new file mode 100644 index 0000000..6426726 --- /dev/null +++ b/apps/google/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import path + +from apps.google.views import GoogleView + +urlpatterns = [ + path("", GoogleView.as_view(), name='google'), +] diff --git a/apps/google/views.py b/apps/google/views.py index 91ea44a..f778d04 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -1,3 +1,11 @@ from django.shortcuts import render +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status # Create your views here. + + +class GoogleView(APIView): + def post(self, request): + return Response({'message': 'Google View!'}, status=status.HTTP_200_OK) diff --git a/project/urls.py b/project/urls.py index fe69e63..c43c761 100644 --- a/project/urls.py +++ b/project/urls.py @@ -5,6 +5,7 @@ from apps.custom_auth.views import CustomAuthToken urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("apps.accounts.urls")), + path("google/", include("apps.google.urls")), path("cases/", include("apps.cases.urls")), path("logs/", include("apps.logs.urls")), path("auth/token/", CustomAuthToken.as_view(), name="api_token_auth"), -- GitLab From a35836b8326bc626f8256b87b2ae5f40e7b7f954 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 01:45:12 +0700 Subject: [PATCH 03/43] [CHORES] install httmock and requests --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 342a229..5447e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,3 +33,5 @@ text-unidecode==1.3 toml==0.10.0 typed-ast==1.4.1 unicodecsv==0.14.1 +httmock==1.4.0 +requests==2.25.1 \ No newline at end of file -- GitLab From 6022c5699bd7efa8374991649a29d39206f7c9f9 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 02:14:18 +0700 Subject: [PATCH 04/43] [RED] test mock google apis --- apps/google/tests/test_units/test_google.py | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index c06db6c..83196ca 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -1,8 +1,19 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status +from httmock import urlmatch, HTTMock + import json +import requests +@urlmatch(netloc=r'https:\/\/www\.googleapis\.com\/oauth2\/v2\/userinfo(.*)') +def googleapis_mock(url, request): + return { + "id": "somerandomid", + "email": "tbcare@tbcare.com", + "verified_email": True, + "picture": "somepathtopicture", + } class GoogleViewTest(APITestCase): @classmethod @@ -12,11 +23,16 @@ class GoogleViewTest(APITestCase): def setUp(self): self.noAuthClient = APIClient() - def test_google_view(self): + def test_google_view_can_access_googleapis(self): url = self.BASE_URL - response = self.noAuthClient.post(url) data = { - "message": "Google View!" + "token": "somerandominputtoken123" } + + response = self.noAuthClient.post(path=url, data=data, format="json") + + with HTTMock(googleapis_mock): + r = requests.get(f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertJSONEqual(json.dumps(response.data), data) + self.assertJSONEqual(response.data['username'], r['email']) -- GitLab From b2fa630a21cef73a0e96002bddf6d0be65d424ae Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:08:13 +0700 Subject: [PATCH 05/43] [RED] fixing mock bug --- apps/google/tests/test_units/test_google.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 83196ca..6290eb7 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -1,19 +1,21 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status -from httmock import urlmatch, HTTMock +from httmock import all_requests, HTTMock, response import json import requests -@urlmatch(netloc=r'https:\/\/www\.googleapis\.com\/oauth2\/v2\/userinfo(.*)') + +@all_requests def googleapis_mock(url, request): - return { + return response(200, { "id": "somerandomid", "email": "tbcare@tbcare.com", "verified_email": True, "picture": "somepathtopicture", - } + }, headers={'Content-Type': 'application/json'}) + class GoogleViewTest(APITestCase): @classmethod @@ -29,10 +31,12 @@ class GoogleViewTest(APITestCase): "token": "somerandominputtoken123" } - response = self.noAuthClient.post(path=url, data=data, format="json") with HTTMock(googleapis_mock): - r = requests.get(f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + response = self.noAuthClient.post(path=url, data=data, format="json") + r = requests.get( + f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertJSONEqual(response.data['username'], r['email']) + -- GitLab From 7689d85ed45ed1e6f35cb0b3a0f89cb55f6a3bc4 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:19:25 +0700 Subject: [PATCH 06/43] [RED] fixing mock bug again --- apps/google/tests/test_units/test_google.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 6290eb7..a8bd26a 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -34,9 +34,10 @@ class GoogleViewTest(APITestCase): with HTTMock(googleapis_mock): response = self.noAuthClient.post(path=url, data=data, format="json") - r = requests.get( + mocked_res = requests.get( f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertJSONEqual(response.data['username'], r['email']) + self.assertEqual(response.data['username'], mocked_data['email']) -- GitLab From 070a82364e693702421d26998226be5cb469f8a2 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:20:16 +0700 Subject: [PATCH 07/43] [GREEN] GoogleView can get data from googleapis --- apps/google/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/google/views.py b/apps/google/views.py index f778d04..d378f0c 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -3,9 +3,13 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status -# Create your views here. +import json +import requests +GOOGLE_API_URL = "https://www.googleapis.com/oauth2/v2/userinfo" class GoogleView(APIView): def post(self, request): - return Response({'message': 'Google View!'}, status=status.HTTP_200_OK) + res = requests.get(GOOGLE_API_URL + "?access_token={data['token']}") + data = json.loads(res.text) + return Response({'username': data['email']}, status=status.HTTP_200_OK) -- GitLab From 852359b784bbf54e820489e6a53e56f3c3e1d393 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:22:42 +0700 Subject: [PATCH 08/43] [REFACTOR] refactor with lint.sh --- apps/google/tests/test_units/test_google.py | 2 -- apps/google/views.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index a8bd26a..a4c4e92 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -31,7 +31,6 @@ class GoogleViewTest(APITestCase): "token": "somerandominputtoken123" } - with HTTMock(googleapis_mock): response = self.noAuthClient.post(path=url, data=data, format="json") mocked_res = requests.get( @@ -40,4 +39,3 @@ class GoogleViewTest(APITestCase): mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['username'], mocked_data['email']) - diff --git a/apps/google/views.py b/apps/google/views.py index d378f0c..57ec8ba 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -8,6 +8,7 @@ import requests GOOGLE_API_URL = "https://www.googleapis.com/oauth2/v2/userinfo" + class GoogleView(APIView): def post(self, request): res = requests.get(GOOGLE_API_URL + "?access_token={data['token']}") -- GitLab From 2d36abd92b5da5b58eb07d8a79e22cf84c1ac0ec Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:32:05 +0700 Subject: [PATCH 09/43] [GREEN] GoogleView can get data from googleapis --- apps/google/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/google/views.py b/apps/google/views.py index 57ec8ba..eaff50e 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -11,6 +11,7 @@ GOOGLE_API_URL = "https://www.googleapis.com/oauth2/v2/userinfo" class GoogleView(APIView): def post(self, request): - res = requests.get(GOOGLE_API_URL + "?access_token={data['token']}") + token = request.data.get('token') + res = requests.get(GOOGLE_API_URL + f"?access_token={token}") data = json.loads(res.text) return Response({'username': data['email']}, status=status.HTTP_200_OK) -- GitLab From 00489ba6e16d07186af55956fab389a16f64fd2f Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:36:12 +0700 Subject: [PATCH 10/43] [REFCATOR] move constant to seperate file --- apps/google/constants.py | 1 + apps/google/tests/test_units/test_google.py | 1 + apps/google/views.py | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 apps/google/constants.py diff --git a/apps/google/constants.py b/apps/google/constants.py new file mode 100644 index 0000000..a09dd14 --- /dev/null +++ b/apps/google/constants.py @@ -0,0 +1 @@ +GOOGLE_API_URL = "https://www.googleapis.com/oauth2/v2/userinfo" diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index a4c4e92..029327f 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -2,6 +2,7 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status from httmock import all_requests, HTTMock, response +from apps.google.constants import GOOGLE_API_URL import json import requests diff --git a/apps/google/views.py b/apps/google/views.py index eaff50e..c35b315 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -2,12 +2,11 @@ from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from apps.google.constants import GOOGLE_API_URL import json import requests -GOOGLE_API_URL = "https://www.googleapis.com/oauth2/v2/userinfo" - class GoogleView(APIView): def post(self, request): -- GitLab From af169cd52165cebbd59c4052f0836d3eeee69275 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:56:22 +0700 Subject: [PATCH 11/43] [RED] negative test, when the given google token is bad --- apps/google/tests/test_units/test_google.py | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 029327f..b3f2db3 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -10,6 +10,15 @@ import requests @all_requests def googleapis_mock(url, request): + print(url) + if ('somebadtoken123' in url.query): + return response(401, { + "error": { + "code": 401, + "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", + "status": "UNAUTHENTICATED" + } + }) return response(200, { "id": "somerandomid", "email": "tbcare@tbcare.com", @@ -40,3 +49,18 @@ class GoogleViewTest(APITestCase): mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['username'], mocked_data['email']) + + def test_google_view_cannot_access_googleapis(self): + url = self.BASE_URL + data = { + "token": "somebadtoken123" + } + + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + + mocked_data = json.loads(mocked_res.content) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.data['error'], mocked_data['error']) -- GitLab From 5f27c1cd1e69a5ae492f1dfc5edbf51246ea48fb Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 04:56:47 +0700 Subject: [PATCH 12/43] [GREEN] negative test, when the given google token is bad --- apps/google/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/google/views.py b/apps/google/views.py index c35b315..3a51e81 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -12,5 +12,7 @@ class GoogleView(APIView): def post(self, request): token = request.data.get('token') res = requests.get(GOOGLE_API_URL + f"?access_token={token}") + if ('error' in res.text): + return Response(json.loads(res.text), status=status.HTTP_401_UNAUTHORIZED) data = json.loads(res.text) return Response({'username': data['email']}, status=status.HTTP_200_OK) -- GitLab From 458147dd17ebe0fcde96cc49c510abffe93f757b Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 05:02:26 +0700 Subject: [PATCH 13/43] [REFACTOR] fixing line limit --- apps/google/tests/test_units/test_google.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index b3f2db3..75eb1fe 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -15,7 +15,10 @@ def googleapis_mock(url, request): return response(401, { "error": { "code": 401, - "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", + "message": "Request is missing required authentication credential." + + " Expected OAuth 2 access token, login cookie or other valid" + + " authentication credential." + + " See https://developers.google.com/identity/sign-in/web/devconsole-project.", "status": "UNAUTHENTICATED" } }) -- GitLab From 24db9a4df5c4e83b6b4a747f2f7141f71596e0c0 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 10 May 2021 05:03:40 +0700 Subject: [PATCH 14/43] [REFACTOR] remove manual debug print command --- apps/google/tests/test_units/test_google.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 75eb1fe..a1ba8f2 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -10,7 +10,6 @@ import requests @all_requests def googleapis_mock(url, request): - print(url) if ('somebadtoken123' in url.query): return response(401, { "error": { -- GitLab From 0e944784eb73835e87f41d399bf33a6ea2153cd0 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 14 May 2021 23:17:19 +0700 Subject: [PATCH 15/43] [RED] GoogleView create User functionality --- apps/google/tests/factories/__init__.py | 0 apps/google/tests/factories/google.py | 8 +++ apps/google/tests/test_units/test_google.py | 69 +++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 apps/google/tests/factories/__init__.py create mode 100644 apps/google/tests/factories/google.py diff --git a/apps/google/tests/factories/__init__.py b/apps/google/tests/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/google/tests/factories/google.py b/apps/google/tests/factories/google.py new file mode 100644 index 0000000..7677c7a --- /dev/null +++ b/apps/google/tests/factories/google.py @@ -0,0 +1,8 @@ +from django.contrib.auth.models import User +import factory + +class UserFactory(factory.DjangoModelFactory): + class Meta: + model = User + + username = factory.Sequence(lambda n: "user_" + str(n)) \ No newline at end of file diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index a1ba8f2..f043771 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -1,8 +1,11 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status +from django.contrib.auth.models import User from httmock import all_requests, HTTMock, response + from apps.google.constants import GOOGLE_API_URL +from apps.google.tests.factories.google import UserFactory import json import requests @@ -21,6 +24,13 @@ def googleapis_mock(url, request): "status": "UNAUTHENTICATED" } }) + if ('existingusertoken123' in url.query): + return response(200, { + "id": "existing_user", + "email": "existinguser@tbcare.com", + "verified_email": True, + "picture": "somepathtopicture", + }, headers={'Content-Type': 'application/json'}) return response(200, { "id": "somerandomid", "email": "tbcare@tbcare.com", @@ -33,6 +43,11 @@ class GoogleViewTest(APITestCase): @classmethod def setUpTestData(cls): cls.BASE_URL = "/google/" + cls.existing_user = UserFactory( + username="existing_user", + password="justpass", + email='existinguser@tbcare.com' + ) def setUp(self): self.noAuthClient = APIClient() @@ -66,3 +81,57 @@ class GoogleViewTest(APITestCase): mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.data['error'], mocked_data['error']) + + def test_google_view_create_new_user(self): + url = self.BASE_URL + data = { + "token": "somerandominputtoken123" + } + + before_req_count = User.objects.all().count() + + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + + after_req_count = User.objects.all().count() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(after_req_count, before_req_count + 1) + + def test_google_view_user_with_existing_email(self): + url = self.BASE_URL + data = { + "token": "existingusertoken123" + } + + before_req_count = User.objects.all().count() + + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + + after_req_count = User.objects.all().count() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(after_req_count, before_req_count) + + def test_google_view_create_new_user_fail(self): + url = self.BASE_URL + data = { + "token": "somebadtoken123" + } + + before_req_count = User.objects.all().count() + + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + + after_req_count = User.objects.all().count() + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(after_req_count, before_req_count) -- GitLab From b794150d8527f2f8fdf645af7b2ff686f7a89ca8 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 14 May 2021 23:17:42 +0700 Subject: [PATCH 16/43] [GREEN] GoogleView create User functionality --- apps/google/views.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/google/views.py b/apps/google/views.py index 3a51e81..062ff40 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -1,4 +1,7 @@ from django.shortcuts import render +from django.contrib.auth.models import User +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -15,4 +18,13 @@ class GoogleView(APIView): if ('error' in res.text): return Response(json.loads(res.text), status=status.HTTP_401_UNAUTHORIZED) data = json.loads(res.text) + + try: + user = User.objects.get(email=data['email']) + except User.DoesNotExist: + user = User() + user.username = data['email'] + user.password = make_password(BaseUserManager().make_random_password()) + user.email = data['email'] + user.save() return Response({'username': data['email']}, status=status.HTTP_200_OK) -- GitLab From d781f108fabb7d58baf83a2030777b2c568383a6 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 14 May 2021 23:19:28 +0700 Subject: [PATCH 17/43] [REFACTOR] lint.sh, GoogleView create User functionality --- apps/google/tests/factories/google.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/google/tests/factories/google.py b/apps/google/tests/factories/google.py index 7677c7a..2728586 100644 --- a/apps/google/tests/factories/google.py +++ b/apps/google/tests/factories/google.py @@ -1,8 +1,9 @@ from django.contrib.auth.models import User import factory + class UserFactory(factory.DjangoModelFactory): class Meta: model = User - username = factory.Sequence(lambda n: "user_" + str(n)) \ No newline at end of file + username = factory.Sequence(lambda n: "user_" + str(n)) -- GitLab From 3bf88e9fb1cb5a8259e58c6bcd0aeed2c9c688d7 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 14 May 2021 23:38:26 +0700 Subject: [PATCH 18/43] [REFACTOR] encapsulate repeated commands in tests --- apps/google/tests/test_units/test_google.py | 62 ++++++++------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index f043771..46c6a15 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -52,16 +52,29 @@ class GoogleViewTest(APITestCase): def setUp(self): self.noAuthClient = APIClient() + def _google_view_response(self, url, data): + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"{GOOGLE_API_URL}?access_token={data['token']}") + return response, mocked_res + + def _google_view_count_user(self, url, data): + before_req_count = User.objects.all().count() + with HTTMock(googleapis_mock): + self.noAuthClient.post(path=url, data=data, format="json") + requests.get( + f"{GOOGLE_API_URL}?access_token={data['token']}") + after_req_count = User.objects.all().count() + return before_req_count, after_req_count + def test_google_view_can_access_googleapis(self): url = self.BASE_URL data = { "token": "somerandominputtoken123" } - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + response, mocked_res = self._google_view_response(url, data) mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -73,10 +86,7 @@ class GoogleViewTest(APITestCase): "token": "somebadtoken123" } - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + response, mocked_res = self._google_view_response(url, data) mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -88,17 +98,9 @@ class GoogleViewTest(APITestCase): "token": "somerandominputtoken123" } - before_req_count = User.objects.all().count() - - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") - - after_req_count = User.objects.all().count() + before, after = self._google_view_count_user(url, data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(after_req_count, before_req_count + 1) + self.assertEqual(after, before + 1) def test_google_view_user_with_existing_email(self): url = self.BASE_URL @@ -106,17 +108,9 @@ class GoogleViewTest(APITestCase): "token": "existingusertoken123" } - before_req_count = User.objects.all().count() - - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") + before, after = self._google_view_count_user(url, data) - after_req_count = User.objects.all().count() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(after_req_count, before_req_count) + self.assertEqual(after, before) def test_google_view_create_new_user_fail(self): url = self.BASE_URL @@ -124,14 +118,6 @@ class GoogleViewTest(APITestCase): "token": "somebadtoken123" } - before_req_count = User.objects.all().count() + before, after = self._google_view_count_user(url, data) - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"https://www.googleapis.com/oauth2/v2/userinfo?access_token={data['token']}") - - after_req_count = User.objects.all().count() - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(after_req_count, before_req_count) + self.assertEqual(after, before) -- GitLab From e6a23864cfcdb5229564d49f9fe108a59f9b7c55 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 15 May 2021 00:03:27 +0700 Subject: [PATCH 19/43] [CHORES] install simplejwt for google auth without password --- project/settings.py | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/project/settings.py b/project/settings.py index b3b1219..6fa6261 100644 --- a/project/settings.py +++ b/project/settings.py @@ -134,6 +134,7 @@ REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.TokenAuthentication", + 'rest_framework_simplejwt.authentication.JWTAuthentication', ], } diff --git a/requirements.txt b/requirements.txt index 5447e01..b33fc1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ django-filter==2.2.0 django-sendgrid-v5==0.8.1 djangorestframework==3.11.0 djangorestframework-csv==2.1.0 +djangorestframework-simplejwt==4.6.0 entrypoints==0.3 factory-boy==2.12.0 Faker==4.0.1 -- GitLab From e0ea968d9eda7f0e1f2021067b7934f1218b3230 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 15 May 2021 19:20:09 +0700 Subject: [PATCH 20/43] [RED] GoogleView can authenticate user, return JWT --- apps/google/tests/test_units/test_google.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 46c6a15..f0d1594 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -1,5 +1,6 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status +from rest_framework_simplejwt.authentication import JWTAuthentication from django.contrib.auth.models import User from httmock import all_requests, HTTMock, response @@ -43,6 +44,7 @@ class GoogleViewTest(APITestCase): @classmethod def setUpTestData(cls): cls.BASE_URL = "/google/" + cls.authenticator = JWTAuthentication() cls.existing_user = UserFactory( username="existing_user", password="justpass", @@ -68,6 +70,11 @@ class GoogleViewTest(APITestCase): after_req_count = User.objects.all().count() return before_req_count, after_req_count + def _get_user_from_token(self, token): + validated_token = self.authenticator.get_validated_token(token) + user = self.authenticator.get_user(validated_token) + return user + def test_google_view_can_access_googleapis(self): url = self.BASE_URL data = { @@ -121,3 +128,29 @@ class GoogleViewTest(APITestCase): before, after = self._google_view_count_user(url, data) self.assertEqual(after, before) + + def test_google_view_jwt_new_user(self): + url = self.BASE_URL + data = { + "token": "somerandominputtoken123" + } + + response, mocked_res = self._google_view_response(url, data) + + res_user = User.objects.get(email="tbcare@tbcare.com") + jwt_user = self._get_user_from_token(response.data['access_token']) + + self.assertEqual(res_user, jwt_user) + + def test_google_view_jwt_existing_user(self): + url = self.BASE_URL + data = { + "token": "existingusertoken123" + } + + response, mocked_res = self._google_view_response(url, data) + + res_user = User.objects.get(email="existinguser@tbcare.com") + jwt_user = self._get_user_from_token(response.data['access_token']) + + self.assertEqual(res_user, jwt_user) -- GitLab From 709ba7cbab58106fd48e4deae95662918a8d3602 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 15 May 2021 19:20:39 +0700 Subject: [PATCH 21/43] [GREEN] GoogleView can authenticate user, return JWT --- apps/google/views.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/google/views.py b/apps/google/views.py index 062ff40..9ac8729 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -5,6 +5,7 @@ from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from rest_framework_simplejwt.tokens import RefreshToken from apps.google.constants import GOOGLE_API_URL import json @@ -12,6 +13,7 @@ import requests class GoogleView(APIView): + def post(self, request): token = request.data.get('token') res = requests.get(GOOGLE_API_URL + f"?access_token={token}") @@ -27,4 +29,12 @@ class GoogleView(APIView): user.password = make_password(BaseUserManager().make_random_password()) user.email = data['email'] user.save() - return Response({'username': data['email']}, status=status.HTTP_200_OK) + + token = RefreshToken.for_user(user) + + response = {} + response['username'] = user.username + response['access_token'] = str(token.access_token) + response['refresh_token'] = str(token) + + return Response(response, status=status.HTTP_200_OK) -- GitLab From 13daba15bdd4f228026f0bd0afe0b6b6496aa252 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 15 May 2021 19:34:49 +0700 Subject: [PATCH 22/43] [CHORES] try to resolve dependency conflict about djangorestframework-simplejwt version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b33fc1a..6487e07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ django-filter==2.2.0 django-sendgrid-v5==0.8.1 djangorestframework==3.11.0 djangorestframework-csv==2.1.0 -djangorestframework-simplejwt==4.6.0 +djangorestframework-simplejwt>=4.4.0 entrypoints==0.3 factory-boy==2.12.0 Faker==4.0.1 -- GitLab From 572bdbe9a171bc4012f69b781a1e0cee0b3d8bf1 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 15 May 2021 19:45:52 +0700 Subject: [PATCH 23/43] [CHORES] try to resolve dependency conflict about djangorestframework-simplejwt and PyJWT version --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6487e07..a87858e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ pathspec==0.7.0 psycopg2-binary==2.8.4 pycodestyle==2.5.0 pyflakes==2.1.1 +PyJWT==1.7.1 python-dateutil==2.8.1 python-dotenv==0.11.0 python-http-client==3.2.7 -- GitLab From a52a2112accfbe99618c3984c39f0e9f37b1e4a1 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Mon, 17 May 2021 05:00:31 +0700 Subject: [PATCH 24/43] [CHORES] experimenting create account with google functionality --- apps/accounts/serializers.py | 59 ++++++++++++++++++++++++++++++++++++ apps/accounts/views.py | 38 +++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index cdae7ac..8a05be1 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -76,3 +76,62 @@ class AccountRegisterSerializer(serializers.ModelSerializer): def to_representation(self, instance): serializer = AccountSerializer(instance) return serializer.data + +class AccountGoogleSerializer(serializers.ModelSerializer): + username = serializers.CharField(source="user.username", read_only=True) + email = serializers.CharField(source="user.email", read_only=True) + + class Meta: + model = Account + fields = [ + "id", + "username", + "name", + "email", + "phone_number", + "district", + "sub_district", + "is_admin", + "is_verified", + "is_active", + ] + + read_only_fields = [ + "id", + "username", + "email", + ] + + def validate(self, data): + district = data['district'] + sub_district = data['sub_district'] + if sub_district not in SUB_DISTRICT_MAPPING[district]: + raise ValidationError(_('Inconsistent district and sub district value')) + return data + + def save(self): + account = self.context.get('request').user.account + super(AccountSerializer, self).save(author=account) + +class AccountGoogleRegisterSerializer(serializers.ModelSerializer): + + class Meta: + model = Account + fields = [ + "id", + "name", + "phone_number", + "district", + "sub_district", + ] + + def validate(self, data): + district = data['district'] + sub_district = data['sub_district'] + if sub_district not in SUB_DISTRICT_MAPPING[district]: + raise ValidationError(_('Inconsistent district and sub district value')) + return data + + def to_representation(self, instance): + serializer = AccountGoogleSerializer(instance) + return serializer.data diff --git a/apps/accounts/views.py b/apps/accounts/views.py index c42086c..9886ef7 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -18,6 +18,7 @@ from apps.accounts.filters import ( from apps.accounts.models import Account from apps.accounts.serializers import ( AccountSerializer, + AccountGoogleRegisterSerializer, AccountRegisterSerializer, ) from apps.commons.permissions import ( @@ -58,6 +59,8 @@ class AccountViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.action in ['create']: return AccountRegisterSerializer + if self.action in ['google']: + return AccountGoogleRegisterSerializer return AccountSerializer def create(self, request): @@ -123,3 +126,38 @@ class AccountViewSet(viewsets.ModelViewSet): serializer = AccountSerializer(instance) return Response(serializer.data, status=status.HTTP_200_OK) + + @action(detail=False, methods=["post"], url_path="google", url_name="google") + def google(self, request): + serializer_class = self.get_serializer_class() + print(serializer_class) + serializer = serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + user = request.user + + if Account.objects.filter(user=user).exists(): + return Response( + {"account": [ + "Account for this email already exists, please relogin without Google." + ]}, + status=status.HTTP_409_CONFLICT + ) + + try: + Account.objects.create( + user=user, + email=user.email, + is_admin=False, + is_verified=False, + is_active=False, + **serializer.validated_data) + except ValidationError as e: + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) + + print(serializer.data) + return Response( + serializer.data, status=status.HTTP_201_CREATED, + ) -- GitLab From 6167cbb0af0ba3f5da8f87a1d1e30a6cf4e5aa87 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 13:35:56 +0700 Subject: [PATCH 25/43] [CHORES] Change permission classes for google endpoint --- apps/accounts/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 9886ef7..e1ec46f 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -7,6 +7,7 @@ from rest_framework.decorators import action from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated as DefaultIsAuthenticated from django.core.exceptions import ValidationError from django.db import transaction @@ -63,6 +64,13 @@ class AccountViewSet(viewsets.ModelViewSet): return AccountGoogleRegisterSerializer return AccountSerializer + def get_permissions(self): + if self.action in ['google']: + self.permission_classes = [DefaultIsAuthenticated,] + else : + self.permission_classes = (IsAuthenticated | CreateOnly, ) + return super().get_permissions() + def create(self, request): serializer_class = self.get_serializer_class() serializer = serializer_class(data=request.data) @@ -130,7 +138,6 @@ class AccountViewSet(viewsets.ModelViewSet): @action(detail=False, methods=["post"], url_path="google", url_name="google") def google(self, request): serializer_class = self.get_serializer_class() - print(serializer_class) serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) @@ -157,7 +164,6 @@ class AccountViewSet(viewsets.ModelViewSet): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - print(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, ) -- GitLab From c92708d61ae0ac4164fd7b18fed5ce0ece77591a Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 19:56:51 +0700 Subject: [PATCH 26/43] [CHORES] add test to untested functionality --- .../tests/test_units/test_accounts.py | 86 ++++++++++++++++++- .../tests/test_units/test_serializers.py | 59 +++++++++++++ apps/constants.py | 1 + 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index 794232d..0efb195 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -6,12 +6,14 @@ 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 rest_framework_simplejwt.tokens import RefreshToken from unittest.mock import patch from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory from apps.accounts.models import Account, AccountHistory from apps.constants import ( HEADER_PREFIX, + JWT_PREFIX, ACTIVITY_TYPE_CREATE, ACTIVITY_TYPE_EDIT, ACTIVITY_TYPE_DELETE, @@ -33,16 +35,44 @@ class AccountViewTest(APITestCase): cls.user_2 = UserFactory(username="user_2", password="justpass") cls.admin = AccountFactory(admin=True, user=cls.user_1) cls.officer = AccountFactory(admin=False, user=cls.user_2) - cls.accounts = [cls.admin, cls.officer] cls.token_1, _ = Token.objects.get_or_create(user=cls.user_1) cls.token_2, _ = Token.objects.get_or_create(user=cls.user_2) + cls.google_user_1 = UserFactory( + username="user@gmail.com", + email="user@gmail.com", + password="justpass" + ) + cls.google_user_2 = UserFactory( + username="user2@gmail.com", + email="user2@gmail.com", + password="justpass" + ) + + cls.jwt_1 = RefreshToken.for_user(cls.google_user_1) + cls.jwt_access_token_1 = str(cls.jwt_1.access_token) + cls.jwt_refresh_token_1 = str(cls.jwt_1) + + cls.jwt_2 = RefreshToken.for_user(cls.google_user_2) + cls.jwt_access_token_2 = str(cls.jwt_2.access_token) + cls.jwt_refresh_token_2 = str(cls.jwt_2) + + cls.google_officer = AccountFactory( + admin=False, + user=cls.google_user_2, + email=cls.google_user_2.email + ) + + cls.accounts = [cls.admin, cls.officer, cls.google_officer] + cls.faker = Faker() def setUp(self): self.noAuthClient = APIClient() self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key) + self.jwt_client_1 = APIClient(HTTP_AUTHORIZATION=JWT_PREFIX + self.jwt_access_token_1) + self.jwt_client_2 = APIClient(HTTP_AUTHORIZATION=JWT_PREFIX + self.jwt_access_token_2) def test_string_representation(self): admin_str = f"[Admin] {self.admin.user.username}" @@ -263,6 +293,60 @@ class AccountViewTest(APITestCase): response = self.client.post(path=url, data=data, format="json",) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_google_create_account_success(self): + url = self.BASE_URL + "google/" + + officer_prev_count = Account.objects.filter(is_admin=False).count() + + data = { + "name": self.officer.name, + "phone_number": "+999999999999", + "district": self.officer.district, + "sub_district": self.officer.sub_district, + } + + response = self.jwt_client_1.post(path=url, data=data, format="json",) + officer_current_count = Account.objects.filter(is_admin=False).count() + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(officer_current_count, officer_prev_count + 1) + + def test_google_create_account_with_existing_email_should_fail(self): + url = self.BASE_URL + "google/" + + officer_prev_count = Account.objects.filter(is_admin=False).count() + + data = { + "name": self.officer.name, + "phone_number": "+999999999999", + "district": self.officer.district, + "sub_district": self.officer.sub_district, + } + + response = self.jwt_client_2.post(path=url, data=data, format="json",) + officer_current_count = Account.objects.filter(is_admin=False).count() + + self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + self.assertEqual(officer_current_count, officer_prev_count) + + def test_google_create_account_with_inconsistent_district_and_sub_district_value(self): + url = self.BASE_URL + "google/" + + officer_prev_count = Account.objects.filter(is_admin=False).count() + + data = { + "name": self.officer.name, + "phone_number": "+999999999999", + "district": 'Beji', + "sub_district": 'Kalibaru', + } + + response = self.jwt_client_1.post(path=url, data=data, format="json",) + officer_current_count = Account.objects.filter(is_admin=False).count() + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(officer_current_count, officer_prev_count) + def test_edit_account_success(self): url = self.BASE_URL + str(self.officer.id) + "/" diff --git a/apps/accounts/tests/test_units/test_serializers.py b/apps/accounts/tests/test_units/test_serializers.py index 9a0cdcb..7fe4006 100644 --- a/apps/accounts/tests/test_units/test_serializers.py +++ b/apps/accounts/tests/test_units/test_serializers.py @@ -32,6 +32,29 @@ class AccountSerializerTest(TestCase): data=data).is_valid, raise_exception=True) +class AccountGoogleSerializerTest(TestCase): + def test_google_serializer_inconsistent_district_mapping_should_raise_error( + self): + account = AccountFactory() + data = { + 'id': account.id, + 'name': account.name, + 'district': 'Beji', + 'sub_district': 'Limo', + 'username': account.user.username, + 'email': account.email, + 'phone_number': account.phone_number, + 'is_admin': account.is_admin, + 'is_verified': account.is_verified, + 'is_active': account.is_active, + } + + self.assertRaises( + ValidationError, + AccountSerializer( + data=data).is_valid, + raise_exception=True) + class AccountRegisterSerializerTest(TestCase): def test_account_register_serializer_inconsistent_district_mapping_should_raise_error( @@ -57,6 +80,42 @@ class AccountRegisterSerializerTest(TestCase): data=data).is_valid, raise_exception=True) +class AccountGoogleRegisterSerializerTest(TestCase): + def test_google_register_serializer_inconsistent_district_mapping_should_raise_error( + self): + account = AccountFactory() + data = { + 'id': account.id, + 'name': account.name, + 'district': 'Beji', + 'sub_district': 'Limo', + 'phone_number': account.phone_number, + } + + self.assertRaises( + ValidationError, + AccountRegisterSerializer( + data=data).is_valid, + raise_exception=True) + +class AccountGoogleRegisterSerializerTest(TestCase): + def test_google_register_serializer_inconsistent_district_mapping_should_raise_error( + self): + account = AccountFactory() + data = { + 'id': account.id, + 'name': account.name, + 'district': 'Beji', + 'sub_district': 'Limo', + 'phone_number': account.phone_number, + } + + self.assertRaises( + ValidationError, + AccountRegisterSerializer( + data=data).is_valid, + raise_exception=True) + class AccountChangePasswordSerializerTest(TestCase): def test_account_change_pass_inconsistent_should_raise_error( diff --git a/apps/constants.py b/apps/constants.py index 3ad2fe9..a9044a1 100644 --- a/apps/constants.py +++ b/apps/constants.py @@ -1,5 +1,6 @@ # Request Header HEADER_PREFIX = "Token " +JWT_PREFIX = "Bearer " # Local Timezone TIMEZONE = "Asia/Jakarta" -- GitLab From 3864763fa0893cc71aa0681285347c2830a401d3 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 20:06:37 +0700 Subject: [PATCH 27/43] [REFACTOR] apply lint.sh --- .../migrations/0009_auto_20210516_1918.py | 263 +++++++++++++++++- apps/accounts/serializers.py | 3 + .../tests/test_units/test_accounts.py | 2 +- .../tests/test_units/test_serializers.py | 5 +- apps/accounts/views.py | 2 +- 5 files changed, 269 insertions(+), 6 deletions(-) diff --git a/apps/accounts/migrations/0009_auto_20210516_1918.py b/apps/accounts/migrations/0009_auto_20210516_1918.py index f63d07a..15535c9 100644 --- a/apps/accounts/migrations/0009_auto_20210516_1918.py +++ b/apps/accounts/migrations/0009_auto_20210516_1918.py @@ -13,16 +13,273 @@ class Migration(migrations.Migration): migrations.AddField( model_name='accounthistory', name='last_modified_at', - field=models.DateTimeField(auto_now=True), + field=models.DateTimeField( + auto_now=True), ), migrations.AlterField( model_name='account', name='sub_district', - field=models.CharField(choices=[('Beji', 'Beji'), ('Beji Timur', 'Beji Timur'), ('Kemirimuka', 'Kemirimuka'), ('Kukusan', 'Kukusan'), ('Pondok Cina', 'Pondok Cina'), ('Tanah Baru', 'Tanah Baru'), ('Bojongsari Baru', 'Bojongsari Baru'), ('Bojongsari Lama', 'Bojongsari Lama'), ('Curug', 'Curug'), ('Duren Mekar', 'Duren Mekar'), ('Duren Seribu', 'Duren Seribu'), ('Pondok Petir', 'Pondok Petir'), ('Serua', 'Serua'), ('Cilodong', 'Cilodong'), ('Jatimulya', 'Jatimulya'), ('Kalibaru', 'Kalibaru'), ('Kalimulya', 'Kalimulya'), ('Sukamaju', 'Sukamaju'), ('Cisalak Pasar', 'Cisalak Pasar'), ('Curug', 'Curug'), ('Harjamukti', 'Harjamukti'), ('Mekarsari', 'Mekarsari'), ('Pasir Gunung Selatan', 'Pasir Gunung Selatan'), ('Tugu', 'Tugu'), ('Cinere', 'Cinere'), ('Gandul', 'Gandul'), ('Pangkalan Jati', 'Pangkalan Jati'), ('Pangkalan Jati Baru', 'Pangkalan Jati Baru'), ('Bojong Pondok Terong', 'Bojong Pondok Terong'), ('Cipayung', 'Cipayung'), ('Cipayung Jaya', 'Cipayung Jaya'), ('Pondok Jaya', 'Pondok Jaya'), ('Ratujaya', 'Ratujaya'), ('Grogol', 'Grogol'), ('Krukut', 'Krukut'), ('Limo', 'Limo'), ('Meruyung', 'Meruyung'), ('Depok Jaya', 'Depok'), ('Depok Jaya', 'Depok Jaya'), ('Mampang', 'Mampang'), ('Pancoran Mas', 'Pancoran Mas'), ('Rangkapan Jaya', 'Rangkapan Jaya'), ('Rangkapan Jaya Baru', 'Rangkapan Jaya Baru'), ('Bedahan', 'Bedahan'), ('Cinangka', 'Cinangka'), ('Kedaung', 'Kedaung'), ('Pasir Putih', 'Pasir Putih'), ('Pengasinan', 'Pengasinan'), ('Sawangan Baru', 'Sawangan Baru'), ('Sawangan Lama', 'Sawangan Lama'), ('Abadijaya', 'Abadijaya'), ('Bakti Jaya', 'Bakti Jaya'), ('Cisalak', 'Cisalak'), ('Mekar Jaya', 'Mekar Jaya'), ('Sukmajaya', 'Sukmajaya'), ('Tirtajaya', 'Tirtajaya'), ('Cilangkap', 'Cilangkap'), ('Cimpaeun', 'Cimpaeun'), ('Jatijajar', 'Jatijajar'), ('Leuwinanggung', 'Leuwinanggung'), ('Sukamaju Baru', 'Sukamaju Baru'), ('Sukatani', 'Sukatani'), ('Tapos', 'Tapos')], max_length=128), + field=models.CharField( + choices=[ + ('Beji', + 'Beji'), + ('Beji Timur', + 'Beji Timur'), + ('Kemirimuka', + 'Kemirimuka'), + ('Kukusan', + 'Kukusan'), + ('Pondok Cina', + 'Pondok Cina'), + ('Tanah Baru', + 'Tanah Baru'), + ('Bojongsari Baru', + 'Bojongsari Baru'), + ('Bojongsari Lama', + 'Bojongsari Lama'), + ('Curug', + 'Curug'), + ('Duren Mekar', + 'Duren Mekar'), + ('Duren Seribu', + 'Duren Seribu'), + ('Pondok Petir', + 'Pondok Petir'), + ('Serua', + 'Serua'), + ('Cilodong', + 'Cilodong'), + ('Jatimulya', + 'Jatimulya'), + ('Kalibaru', + 'Kalibaru'), + ('Kalimulya', + 'Kalimulya'), + ('Sukamaju', + 'Sukamaju'), + ('Cisalak Pasar', + 'Cisalak Pasar'), + ('Curug', + 'Curug'), + ('Harjamukti', + 'Harjamukti'), + ('Mekarsari', + 'Mekarsari'), + ('Pasir Gunung Selatan', + 'Pasir Gunung Selatan'), + ('Tugu', + 'Tugu'), + ('Cinere', + 'Cinere'), + ('Gandul', + 'Gandul'), + ('Pangkalan Jati', + 'Pangkalan Jati'), + ('Pangkalan Jati Baru', + 'Pangkalan Jati Baru'), + ('Bojong Pondok Terong', + 'Bojong Pondok Terong'), + ('Cipayung', + 'Cipayung'), + ('Cipayung Jaya', + 'Cipayung Jaya'), + ('Pondok Jaya', + 'Pondok Jaya'), + ('Ratujaya', + 'Ratujaya'), + ('Grogol', + 'Grogol'), + ('Krukut', + 'Krukut'), + ('Limo', + 'Limo'), + ('Meruyung', + 'Meruyung'), + ('Depok Jaya', + 'Depok'), + ('Depok Jaya', + 'Depok Jaya'), + ('Mampang', + 'Mampang'), + ('Pancoran Mas', + 'Pancoran Mas'), + ('Rangkapan Jaya', + 'Rangkapan Jaya'), + ('Rangkapan Jaya Baru', + 'Rangkapan Jaya Baru'), + ('Bedahan', + 'Bedahan'), + ('Cinangka', + 'Cinangka'), + ('Kedaung', + 'Kedaung'), + ('Pasir Putih', + 'Pasir Putih'), + ('Pengasinan', + 'Pengasinan'), + ('Sawangan Baru', + 'Sawangan Baru'), + ('Sawangan Lama', + 'Sawangan Lama'), + ('Abadijaya', + 'Abadijaya'), + ('Bakti Jaya', + 'Bakti Jaya'), + ('Cisalak', + 'Cisalak'), + ('Mekar Jaya', + 'Mekar Jaya'), + ('Sukmajaya', + 'Sukmajaya'), + ('Tirtajaya', + 'Tirtajaya'), + ('Cilangkap', + 'Cilangkap'), + ('Cimpaeun', + 'Cimpaeun'), + ('Jatijajar', + 'Jatijajar'), + ('Leuwinanggung', + 'Leuwinanggung'), + ('Sukamaju Baru', + 'Sukamaju Baru'), + ('Sukatani', + 'Sukatani'), + ('Tapos', + 'Tapos')], + max_length=128), ), migrations.AlterField( model_name='accounthistory', name='sub_district', - field=models.CharField(choices=[('Beji', 'Beji'), ('Beji Timur', 'Beji Timur'), ('Kemirimuka', 'Kemirimuka'), ('Kukusan', 'Kukusan'), ('Pondok Cina', 'Pondok Cina'), ('Tanah Baru', 'Tanah Baru'), ('Bojongsari Baru', 'Bojongsari Baru'), ('Bojongsari Lama', 'Bojongsari Lama'), ('Curug', 'Curug'), ('Duren Mekar', 'Duren Mekar'), ('Duren Seribu', 'Duren Seribu'), ('Pondok Petir', 'Pondok Petir'), ('Serua', 'Serua'), ('Cilodong', 'Cilodong'), ('Jatimulya', 'Jatimulya'), ('Kalibaru', 'Kalibaru'), ('Kalimulya', 'Kalimulya'), ('Sukamaju', 'Sukamaju'), ('Cisalak Pasar', 'Cisalak Pasar'), ('Curug', 'Curug'), ('Harjamukti', 'Harjamukti'), ('Mekarsari', 'Mekarsari'), ('Pasir Gunung Selatan', 'Pasir Gunung Selatan'), ('Tugu', 'Tugu'), ('Cinere', 'Cinere'), ('Gandul', 'Gandul'), ('Pangkalan Jati', 'Pangkalan Jati'), ('Pangkalan Jati Baru', 'Pangkalan Jati Baru'), ('Bojong Pondok Terong', 'Bojong Pondok Terong'), ('Cipayung', 'Cipayung'), ('Cipayung Jaya', 'Cipayung Jaya'), ('Pondok Jaya', 'Pondok Jaya'), ('Ratujaya', 'Ratujaya'), ('Grogol', 'Grogol'), ('Krukut', 'Krukut'), ('Limo', 'Limo'), ('Meruyung', 'Meruyung'), ('Depok Jaya', 'Depok'), ('Depok Jaya', 'Depok Jaya'), ('Mampang', 'Mampang'), ('Pancoran Mas', 'Pancoran Mas'), ('Rangkapan Jaya', 'Rangkapan Jaya'), ('Rangkapan Jaya Baru', 'Rangkapan Jaya Baru'), ('Bedahan', 'Bedahan'), ('Cinangka', 'Cinangka'), ('Kedaung', 'Kedaung'), ('Pasir Putih', 'Pasir Putih'), ('Pengasinan', 'Pengasinan'), ('Sawangan Baru', 'Sawangan Baru'), ('Sawangan Lama', 'Sawangan Lama'), ('Abadijaya', 'Abadijaya'), ('Bakti Jaya', 'Bakti Jaya'), ('Cisalak', 'Cisalak'), ('Mekar Jaya', 'Mekar Jaya'), ('Sukmajaya', 'Sukmajaya'), ('Tirtajaya', 'Tirtajaya'), ('Cilangkap', 'Cilangkap'), ('Cimpaeun', 'Cimpaeun'), ('Jatijajar', 'Jatijajar'), ('Leuwinanggung', 'Leuwinanggung'), ('Sukamaju Baru', 'Sukamaju Baru'), ('Sukatani', 'Sukatani'), ('Tapos', 'Tapos')], max_length=128), + field=models.CharField( + choices=[ + ('Beji', + 'Beji'), + ('Beji Timur', + 'Beji Timur'), + ('Kemirimuka', + 'Kemirimuka'), + ('Kukusan', + 'Kukusan'), + ('Pondok Cina', + 'Pondok Cina'), + ('Tanah Baru', + 'Tanah Baru'), + ('Bojongsari Baru', + 'Bojongsari Baru'), + ('Bojongsari Lama', + 'Bojongsari Lama'), + ('Curug', + 'Curug'), + ('Duren Mekar', + 'Duren Mekar'), + ('Duren Seribu', + 'Duren Seribu'), + ('Pondok Petir', + 'Pondok Petir'), + ('Serua', + 'Serua'), + ('Cilodong', + 'Cilodong'), + ('Jatimulya', + 'Jatimulya'), + ('Kalibaru', + 'Kalibaru'), + ('Kalimulya', + 'Kalimulya'), + ('Sukamaju', + 'Sukamaju'), + ('Cisalak Pasar', + 'Cisalak Pasar'), + ('Curug', + 'Curug'), + ('Harjamukti', + 'Harjamukti'), + ('Mekarsari', + 'Mekarsari'), + ('Pasir Gunung Selatan', + 'Pasir Gunung Selatan'), + ('Tugu', + 'Tugu'), + ('Cinere', + 'Cinere'), + ('Gandul', + 'Gandul'), + ('Pangkalan Jati', + 'Pangkalan Jati'), + ('Pangkalan Jati Baru', + 'Pangkalan Jati Baru'), + ('Bojong Pondok Terong', + 'Bojong Pondok Terong'), + ('Cipayung', + 'Cipayung'), + ('Cipayung Jaya', + 'Cipayung Jaya'), + ('Pondok Jaya', + 'Pondok Jaya'), + ('Ratujaya', + 'Ratujaya'), + ('Grogol', + 'Grogol'), + ('Krukut', + 'Krukut'), + ('Limo', + 'Limo'), + ('Meruyung', + 'Meruyung'), + ('Depok Jaya', + 'Depok'), + ('Depok Jaya', + 'Depok Jaya'), + ('Mampang', + 'Mampang'), + ('Pancoran Mas', + 'Pancoran Mas'), + ('Rangkapan Jaya', + 'Rangkapan Jaya'), + ('Rangkapan Jaya Baru', + 'Rangkapan Jaya Baru'), + ('Bedahan', + 'Bedahan'), + ('Cinangka', + 'Cinangka'), + ('Kedaung', + 'Kedaung'), + ('Pasir Putih', + 'Pasir Putih'), + ('Pengasinan', + 'Pengasinan'), + ('Sawangan Baru', + 'Sawangan Baru'), + ('Sawangan Lama', + 'Sawangan Lama'), + ('Abadijaya', + 'Abadijaya'), + ('Bakti Jaya', + 'Bakti Jaya'), + ('Cisalak', + 'Cisalak'), + ('Mekar Jaya', + 'Mekar Jaya'), + ('Sukmajaya', + 'Sukmajaya'), + ('Tirtajaya', + 'Tirtajaya'), + ('Cilangkap', + 'Cilangkap'), + ('Cimpaeun', + 'Cimpaeun'), + ('Jatijajar', + 'Jatijajar'), + ('Leuwinanggung', + 'Leuwinanggung'), + ('Sukamaju Baru', + 'Sukamaju Baru'), + ('Sukatani', + 'Sukatani'), + ('Tapos', + 'Tapos')], + max_length=128), ), ] diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 293015e..6b2f012 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -77,6 +77,7 @@ class AccountRegisterSerializer(serializers.ModelSerializer): serializer = AccountSerializer(instance) return serializer.data + class AccountGoogleSerializer(serializers.ModelSerializer): username = serializers.CharField(source="user.username", read_only=True) email = serializers.CharField(source="user.email", read_only=True) @@ -113,6 +114,7 @@ class AccountGoogleSerializer(serializers.ModelSerializer): account = self.context.get('request').user.account super(AccountSerializer, self).save(author=account) + class AccountGoogleRegisterSerializer(serializers.ModelSerializer): class Meta: @@ -136,6 +138,7 @@ class AccountGoogleRegisterSerializer(serializers.ModelSerializer): serializer = AccountGoogleSerializer(instance) return serializer.data + class AccountChangePasswordSerializer(serializers.Serializer): new_password = serializers.CharField(max_length=128) confirm_new_password = serializers.CharField(max_length=128) diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index 0efb195..b91115a 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -59,7 +59,7 @@ class AccountViewTest(APITestCase): cls.jwt_refresh_token_2 = str(cls.jwt_2) cls.google_officer = AccountFactory( - admin=False, + admin=False, user=cls.google_user_2, email=cls.google_user_2.email ) diff --git a/apps/accounts/tests/test_units/test_serializers.py b/apps/accounts/tests/test_units/test_serializers.py index 7fe4006..ec167bd 100644 --- a/apps/accounts/tests/test_units/test_serializers.py +++ b/apps/accounts/tests/test_units/test_serializers.py @@ -32,6 +32,7 @@ class AccountSerializerTest(TestCase): data=data).is_valid, raise_exception=True) + class AccountGoogleSerializerTest(TestCase): def test_google_serializer_inconsistent_district_mapping_should_raise_error( self): @@ -80,6 +81,7 @@ class AccountRegisterSerializerTest(TestCase): data=data).is_valid, raise_exception=True) + class AccountGoogleRegisterSerializerTest(TestCase): def test_google_register_serializer_inconsistent_district_mapping_should_raise_error( self): @@ -97,7 +99,8 @@ class AccountGoogleRegisterSerializerTest(TestCase): AccountRegisterSerializer( data=data).is_valid, raise_exception=True) - + + class AccountGoogleRegisterSerializerTest(TestCase): def test_google_register_serializer_inconsistent_district_mapping_should_raise_error( self): diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 42807f6..b207e0f 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -200,7 +200,7 @@ class AccountViewSet(viewsets.ModelViewSet): serializer_class = self.get_serializer_class() serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - + user = request.user if Account.objects.filter(user=user).exists(): return Response( -- GitLab From 90a738903c35daeef4d4f39a8500028e63c85676 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 21:56:54 +0700 Subject: [PATCH 28/43] [RED] GoogleView response with JWT only when related Account exists --- apps/google/tests/test_units/test_google.py | 74 ++++++--------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index f0d1594..d2320cd 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -6,7 +6,10 @@ from django.contrib.auth.models import User from httmock import all_requests, HTTMock, response from apps.google.constants import GOOGLE_API_URL -from apps.google.tests.factories.google import UserFactory +from apps.accounts.tests.factories.accounts import ( + UserFactory, + AccountFactory +) import json import requests @@ -46,10 +49,13 @@ class GoogleViewTest(APITestCase): cls.BASE_URL = "/google/" cls.authenticator = JWTAuthentication() cls.existing_user = UserFactory( - username="existing_user", + username="existinguser@tbcare.com", password="justpass", email='existinguser@tbcare.com' ) + cls.existing_account = AccountFactory( + admin=False, user=cls.existing_user + ) def setUp(self): self.noAuthClient = APIClient() @@ -61,21 +67,12 @@ class GoogleViewTest(APITestCase): f"{GOOGLE_API_URL}?access_token={data['token']}") return response, mocked_res - def _google_view_count_user(self, url, data): - before_req_count = User.objects.all().count() - with HTTMock(googleapis_mock): - self.noAuthClient.post(path=url, data=data, format="json") - requests.get( - f"{GOOGLE_API_URL}?access_token={data['token']}") - after_req_count = User.objects.all().count() - return before_req_count, after_req_count - def _get_user_from_token(self, token): validated_token = self.authenticator.get_validated_token(token) user = self.authenticator.get_user(validated_token) return user - def test_google_view_can_access_googleapis(self): + def test_new_user_can_access_googleapis(self): url = self.BASE_URL data = { "token": "somerandominputtoken123" @@ -84,65 +81,34 @@ class GoogleViewTest(APITestCase): response, mocked_res = self._google_view_response(url, data) mocked_data = json.loads(mocked_res.content) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['username'], mocked_data['email']) + self.assertEqual(response.status_code, status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) + self.assertEqual(response.data, mocked_data) - def test_google_view_cannot_access_googleapis(self): + def test_existing_user_can_access_googleapis(self): url = self.BASE_URL data = { - "token": "somebadtoken123" + "token": "existingusertoken123" } response, mocked_res = self._google_view_response(url, data) mocked_data = json.loads(mocked_res.content) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.data['error'], mocked_data['error']) - - def test_google_view_create_new_user(self): - url = self.BASE_URL - data = { - "token": "somerandominputtoken123" - } - - before, after = self._google_view_count_user(url, data) - - self.assertEqual(after, before + 1) - - def test_google_view_user_with_existing_email(self): - url = self.BASE_URL - data = { - "token": "existingusertoken123" - } - - before, after = self._google_view_count_user(url, data) - - self.assertEqual(after, before) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['username'], mocked_data['email']) - def test_google_view_create_new_user_fail(self): + def test_bad_token_cannot_access_googleapis(self): url = self.BASE_URL data = { "token": "somebadtoken123" } - before, after = self._google_view_count_user(url, data) - - self.assertEqual(after, before) - - def test_google_view_jwt_new_user(self): - url = self.BASE_URL - data = { - "token": "somerandominputtoken123" - } - response, mocked_res = self._google_view_response(url, data) - res_user = User.objects.get(email="tbcare@tbcare.com") - jwt_user = self._get_user_from_token(response.data['access_token']) - - self.assertEqual(res_user, jwt_user) + mocked_data = json.loads(mocked_res.content) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.data['error'], mocked_data['error']) - def test_google_view_jwt_existing_user(self): + def test_existing_user_get_jwt(self): url = self.BASE_URL data = { "token": "existingusertoken123" -- GitLab From 8409639940376e5523fec0d379d0a92a31ffd10b Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 21:57:10 +0700 Subject: [PATCH 29/43] [GREEN] GoogleView response with JWT only when related Account exists --- apps/google/views.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/google/views.py b/apps/google/views.py index 9ac8729..9a0424f 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework import status from rest_framework_simplejwt.tokens import RefreshToken from apps.google.constants import GOOGLE_API_URL +from apps.accounts.models import Account import json import requests @@ -23,12 +24,9 @@ class GoogleView(APIView): try: user = User.objects.get(email=data['email']) - except User.DoesNotExist: - user = User() - user.username = data['email'] - user.password = make_password(BaseUserManager().make_random_password()) - user.email = data['email'] - user.save() + accounts = Account.objects.get(user=user) + except (User.DoesNotExist, Account.DoesNotExist): + return Response(data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) token = RefreshToken.for_user(user) -- GitLab From 06ef883379f4768cb2ed5b4286334573a49b2b1f Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Thu, 20 May 2021 22:06:52 +0700 Subject: [PATCH 30/43] [CHORES] add another prefix for simplejwt --- project/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project/settings.py b/project/settings.py index c6c6642..f2e6afc 100644 --- a/project/settings.py +++ b/project/settings.py @@ -126,6 +126,10 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] +SIMPLE_JWT = { + 'AUTH_HEADER_TYPES': ('Bearer', 'Token',), +} + # Pagination REST_FRAMEWORK = { -- GitLab From 2b2f52ca43df2245b6554fb6f749033c4950cb2a Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 22:01:06 +0700 Subject: [PATCH 31/43] [CHORES] remove simplejwt and use previous authtoken instead --- project/settings.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/project/settings.py b/project/settings.py index f2e6afc..7aa71a4 100644 --- a/project/settings.py +++ b/project/settings.py @@ -126,10 +126,6 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -SIMPLE_JWT = { - 'AUTH_HEADER_TYPES': ('Bearer', 'Token',), -} - # Pagination REST_FRAMEWORK = { @@ -138,7 +134,6 @@ REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.TokenAuthentication", - 'rest_framework_simplejwt.authentication.JWTAuthentication', ], } -- GitLab From bffb751c14b8da86b94f4b23e7fe2c436bec8ff4 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 22:57:21 +0700 Subject: [PATCH 32/43] [RED] GoogleView using authtoken instead of simplejwt --- apps/google/tests/test_units/test_google.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index d2320cd..421c5ad 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -1,6 +1,6 @@ from rest_framework.test import APITestCase, APIClient from rest_framework import status -from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework.authtoken.models import Token from django.contrib.auth.models import User from httmock import all_requests, HTTMock, response @@ -47,7 +47,6 @@ class GoogleViewTest(APITestCase): @classmethod def setUpTestData(cls): cls.BASE_URL = "/google/" - cls.authenticator = JWTAuthentication() cls.existing_user = UserFactory( username="existinguser@tbcare.com", password="justpass", @@ -67,11 +66,6 @@ class GoogleViewTest(APITestCase): f"{GOOGLE_API_URL}?access_token={data['token']}") return response, mocked_res - def _get_user_from_token(self, token): - validated_token = self.authenticator.get_validated_token(token) - user = self.authenticator.get_user(validated_token) - return user - def test_new_user_can_access_googleapis(self): url = self.BASE_URL data = { @@ -82,7 +76,7 @@ class GoogleViewTest(APITestCase): mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) - self.assertEqual(response.data, mocked_data) + self.assertTrue(mocked_data.items() <= response.data.items()) def test_existing_user_can_access_googleapis(self): url = self.BASE_URL @@ -94,7 +88,7 @@ class GoogleViewTest(APITestCase): mocked_data = json.loads(mocked_res.content) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['username'], mocked_data['email']) + self.assertTrue(mocked_data.items() <= response.data.items()) def test_bad_token_cannot_access_googleapis(self): url = self.BASE_URL @@ -108,7 +102,7 @@ class GoogleViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.data['error'], mocked_data['error']) - def test_existing_user_get_jwt(self): + def test_existing_user_get_auth_token(self): url = self.BASE_URL data = { "token": "existingusertoken123" @@ -116,7 +110,5 @@ class GoogleViewTest(APITestCase): response, mocked_res = self._google_view_response(url, data) - res_user = User.objects.get(email="existinguser@tbcare.com") - jwt_user = self._get_user_from_token(response.data['access_token']) - - self.assertEqual(res_user, jwt_user) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(Token.objects.filter(user=self.existing_user).exists()) -- GitLab From 6988a6abd92dbb8cf53f36d55efd5dc7e250f53c Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 22:57:40 +0700 Subject: [PATCH 33/43] [GREEN] GoogleView using authtoken instead of simplejwt --- apps/google/views.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/google/views.py b/apps/google/views.py index 9a0424f..cbca52c 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -5,7 +5,7 @@ from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status -from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework.authtoken.models import Token from apps.google.constants import GOOGLE_API_URL from apps.accounts.models import Account @@ -28,11 +28,8 @@ class GoogleView(APIView): except (User.DoesNotExist, Account.DoesNotExist): return Response(data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) - token = RefreshToken.for_user(user) - - response = {} - response['username'] = user.username - response['access_token'] = str(token.access_token) - response['refresh_token'] = str(token) + token, _ = Token.objects.get_or_create(user=user) + response = {**data, "token": token.key} return Response(response, status=status.HTTP_200_OK) + -- GitLab From e134d967c84c8ccbb276d013554e5f9b8cba90a2 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 22:58:39 +0700 Subject: [PATCH 34/43] [RED] Create account with google access token --- .../tests/test_units/test_accounts.py | 107 ++++++++++++------ 1 file changed, 73 insertions(+), 34 deletions(-) diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index b91115a..26d42e6 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -6,8 +6,8 @@ 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 rest_framework_simplejwt.tokens import RefreshToken from unittest.mock import patch +from httmock import all_requests, HTTMock, response from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory from apps.accounts.models import Account, AccountHistory @@ -22,6 +22,40 @@ from apps.cases.constants import ( DISTRICT_CHOICES, SUB_DISTRICT_MAPPING ) +from apps.google.constants import GOOGLE_API_URL + +import json +import requests + + +@all_requests +def googleapis_mock(url, request): + if ('somebadtoken123' in url.query): + return response(401, { + "error": { + "code": 401, + "message": "Request is missing required authentication credential." + + " Expected OAuth 2 access token, login cookie or other valid" + + " authentication credential." + + " See https://developers.google.com/identity/sign-in/web/devconsole-project.", + "status": "UNAUTHENTICATED" + } + }) + if ('existingusertoken123' in url.query): + return response(200, { + "id": "existing_user", + "name": "Existing User", + "email": "existinguser@tbcare.com", + "verified_email": True, + "picture": "somepathtopicture", + }, headers={'Content-Type': 'application/json'}) + return response(200, { + "id": "somerandomid", + "name": "New User", + "email": "newuser@tbcare.com", + "verified_email": True, + "picture": "somepathtopicture", + }, headers={'Content-Type': 'application/json'}) class AccountViewTest(APITestCase): @@ -39,40 +73,27 @@ class AccountViewTest(APITestCase): cls.token_1, _ = Token.objects.get_or_create(user=cls.user_1) cls.token_2, _ = Token.objects.get_or_create(user=cls.user_2) - cls.google_user_1 = UserFactory( - username="user@gmail.com", - email="user@gmail.com", - password="justpass" - ) - cls.google_user_2 = UserFactory( - username="user2@gmail.com", - email="user2@gmail.com", + cls.existing_google_user = UserFactory( + username="existinguser@tbcare.com", + email="existinguser@tbcare.com", password="justpass" ) + cls.existing_google_officer = AccountFactory(admin=False, user=cls.existing_google_user) - cls.jwt_1 = RefreshToken.for_user(cls.google_user_1) - cls.jwt_access_token_1 = str(cls.jwt_1.access_token) - cls.jwt_refresh_token_1 = str(cls.jwt_1) - - cls.jwt_2 = RefreshToken.for_user(cls.google_user_2) - cls.jwt_access_token_2 = str(cls.jwt_2.access_token) - cls.jwt_refresh_token_2 = str(cls.jwt_2) - - cls.google_officer = AccountFactory( - admin=False, - user=cls.google_user_2, - email=cls.google_user_2.email - ) - - cls.accounts = [cls.admin, cls.officer, cls.google_officer] + cls.accounts = [cls.admin, cls.officer, cls.existing_google_officer] cls.faker = Faker() def setUp(self): self.noAuthClient = APIClient() self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key) - self.jwt_client_1 = APIClient(HTTP_AUTHORIZATION=JWT_PREFIX + self.jwt_access_token_1) - self.jwt_client_2 = APIClient(HTTP_AUTHORIZATION=JWT_PREFIX + self.jwt_access_token_2) + + def _google_api_response(self, url, data): + with HTTMock(googleapis_mock): + response = self.noAuthClient.post(path=url, data=data, format="json") + mocked_res = requests.get( + f"{GOOGLE_API_URL}?access_token={data['token']}") + return response, mocked_res def test_string_representation(self): admin_str = f"[Admin] {self.admin.user.username}" @@ -299,13 +320,13 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "name": self.officer.name, + "token": "newusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, } - response = self.jwt_client_1.post(path=url, data=data, format="json",) + response, mocked_res = self._google_api_response(url, data) officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -317,13 +338,13 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "name": self.officer.name, + "token": "existingusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, } - response = self.jwt_client_2.post(path=url, data=data, format="json",) + response, mocked_res = self._google_api_response(url, data) officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) @@ -335,13 +356,31 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "name": self.officer.name, + "token": "newusertoken123", "phone_number": "+999999999999", - "district": 'Beji', - "sub_district": 'Kalibaru', + "district": "Beji", + "sub_district": "Limo", + } + + response, mocked_res = self._google_api_response(url, data) + officer_current_count = Account.objects.filter(is_admin=False).count() + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(officer_current_count, officer_prev_count) + + def test_google_create_account_with_bad_google_token(self): + url = self.BASE_URL + "google/" + + officer_prev_count = Account.objects.filter(is_admin=False).count() + + data = { + "token": "somebadtoken123", + "phone_number": "+999999999999", + "district": self.officer.district, + "sub_district": self.officer.sub_district, } - response = self.jwt_client_1.post(path=url, data=data, format="json",) + response, mocked_res = self._google_api_response(url, data) officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) -- GitLab From 616b0cf57f98e7fccece36d6cd72e1d4221db08c Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 22:58:54 +0700 Subject: [PATCH 35/43] [GREEN] Create account with google access token --- apps/accounts/serializers.py | 4 +++- apps/accounts/views.py | 36 ++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 6b2f012..78b4821 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -101,6 +101,7 @@ class AccountGoogleSerializer(serializers.ModelSerializer): "id", "username", "email", + "name", ] def validate(self, data): @@ -116,12 +117,13 @@ class AccountGoogleSerializer(serializers.ModelSerializer): class AccountGoogleRegisterSerializer(serializers.ModelSerializer): + token = serializers.CharField() class Meta: model = Account fields = [ "id", - "name", + "token", "phone_number", "district", "sub_district", diff --git a/apps/accounts/views.py b/apps/accounts/views.py index b207e0f..1e7e5b3 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -1,6 +1,8 @@ from django.core.mail import send_mail from django.contrib.auth.models import AnonymousUser, User from django_filters.rest_framework import DjangoFilterBackend +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.hashers import make_password from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.filters import SearchFilter, OrderingFilter @@ -43,6 +45,10 @@ from apps.constants import ( EMAIL_PASSWORD_RESET_CONTENT, EMAIL_PASSWORD_RESET_SUBJECT, ) +from apps.google.constants import GOOGLE_API_URL + +import json +import requests class AccountViewSet(viewsets.ModelViewSet): @@ -73,13 +79,6 @@ class AccountViewSet(viewsets.ModelViewSet): return AccountSetRandomPasswordSerializer return AccountSerializer - def get_permissions(self): - if self.action in ['google']: - self.permission_classes = [DefaultIsAuthenticated, ] - else: - self.permission_classes = (IsAuthenticated | CreateOnly, ) - return super().get_permissions() - def create(self, request): serializer_class = self.get_serializer_class() serializer = serializer_class(data=request.data) @@ -201,27 +200,36 @@ class AccountViewSet(viewsets.ModelViewSet): serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - user = request.user - if Account.objects.filter(user=user).exists(): + token = serializer.validated_data.pop("token") + res = requests.get(GOOGLE_API_URL + f"?access_token={token}") + if ('error' in res.text): + return Response(json.loads(res.text), status=status.HTTP_400_BAD_REQUEST) + data = json.loads(res.text) + + if User.objects.filter(username=data['email']).exists(): return Response( - {"account": [ - "Account for this email already exists, please relogin without Google." - ]}, + {"username": ["User with that username already exists."]}, status=status.HTTP_409_CONFLICT ) try: + user = User.objects.create_user( + username=data['email'], + password=make_password(BaseUserManager().make_random_password()) + ) Account.objects.create( user=user, - email=user.email, + name=data['name'], + email=data['email'], is_admin=False, - is_verified=False, is_active=False, + is_verified=False, **serializer.validated_data) except ValidationError as e: return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) + return Response( serializer.data, status=status.HTTP_201_CREATED, ) -- GitLab From 3aaf9ec6e7cc185519c601bf5ea3e40ee3f1ca22 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Fri, 21 May 2021 23:12:00 +0700 Subject: [PATCH 36/43] [CHORES] get user by username instead of email --- apps/google/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/google/views.py b/apps/google/views.py index cbca52c..6fdfb82 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -23,7 +23,7 @@ class GoogleView(APIView): data = json.loads(res.text) try: - user = User.objects.get(email=data['email']) + user = User.objects.get(username=data['email']) accounts = Account.objects.get(user=user) except (User.DoesNotExist, Account.DoesNotExist): return Response(data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) -- GitLab From 2603a563af661d7af54d195667f81d185fcc56ce Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:19:21 +0700 Subject: [PATCH 37/43] [CHORES] add google-api-python-client and GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET --- project/settings.py | 3 +++ requirements.txt | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/project/settings.py b/project/settings.py index 7aa71a4..d91e4b4 100644 --- a/project/settings.py +++ b/project/settings.py @@ -170,3 +170,6 @@ if USE_EMAIL: EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") EMAIL_PORT = os.environ.get("EMAIL_PORT") EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS") + +GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5c9e421..7a3f402 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,12 +11,12 @@ django-filter==2.2.0 django-sendgrid-v5==0.8.1 djangorestframework==3.11.0 djangorestframework-csv==2.1.0 -djangorestframework-simplejwt>=4.4.0 entrypoints==0.3 et-xmlfile==1.1.0 factory-boy==2.12.0 Faker==4.0.1 future==0.18.2 +google-api-python-client==2.6.0 gunicorn==20.0.4 mccabe==0.6.1 numpy==1.19.5 @@ -38,6 +38,4 @@ sqlparse==0.3.0 text-unidecode==1.3 toml==0.10.0 typed-ast==1.4.1 -unicodecsv==0.14.1 -httmock==1.4.0 -requests==2.25.1 \ No newline at end of file +unicodecsv==0.14.1 \ No newline at end of file -- GitLab From 4733546a675f273880bfbc1926f220ffa0ecf1cc Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:20:36 +0700 Subject: [PATCH 38/43] [RED] Google OAuth2, using google-api-python-client instead of manually request to google --- apps/google/tests/test_units/test_google.py | 110 +++++++++----------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index 421c5ad..b2a67bb 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -3,45 +3,13 @@ from rest_framework import status from rest_framework.authtoken.models import Token from django.contrib.auth.models import User -from httmock import all_requests, HTTMock, response +from unittest.mock import patch -from apps.google.constants import GOOGLE_API_URL from apps.accounts.tests.factories.accounts import ( UserFactory, AccountFactory ) -import json -import requests - - -@all_requests -def googleapis_mock(url, request): - if ('somebadtoken123' in url.query): - return response(401, { - "error": { - "code": 401, - "message": "Request is missing required authentication credential." + - " Expected OAuth 2 access token, login cookie or other valid" + - " authentication credential." + - " See https://developers.google.com/identity/sign-in/web/devconsole-project.", - "status": "UNAUTHENTICATED" - } - }) - if ('existingusertoken123' in url.query): - return response(200, { - "id": "existing_user", - "email": "existinguser@tbcare.com", - "verified_email": True, - "picture": "somepathtopicture", - }, headers={'Content-Type': 'application/json'}) - return response(200, { - "id": "somerandomid", - "email": "tbcare@tbcare.com", - "verified_email": True, - "picture": "somepathtopicture", - }, headers={'Content-Type': 'application/json'}) - class GoogleViewTest(APITestCase): @classmethod @@ -55,60 +23,80 @@ class GoogleViewTest(APITestCase): cls.existing_account = AccountFactory( admin=False, user=cls.existing_user ) + cls.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE = { + "iss": "https://accounts.google.com", + "azp": "valid-client-id.apps.googleusercontent.com", + "aud": "valid-client-id.apps.googleusercontent.com", + "sub": "randomsub", + "email": "existinguser@tbcare.com", + "email_verified": True, + "at_hash": "randomhash", + "name": "Random User", + "picture": "profile-image-url", + "given_name": "Random", + "family_name": "User", + "locale": "en-GB", + "iat": 1622290189, + "exp": 1622293789, + } def setUp(self): self.noAuthClient = APIClient() - def _google_view_response(self, url, data): - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"{GOOGLE_API_URL}?access_token={data['token']}") - return response, mocked_res + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_existing_user_get_auth_token(self, id_token_mock): - def test_new_user_can_access_googleapis(self): url = self.BASE_URL data = { - "token": "somerandominputtoken123" + "id_token": "existingusertoken123" } - response, mocked_res = self._google_view_response(url, data) + id_token_mock.return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE - mocked_data = json.loads(mocked_res.content) - self.assertEqual(response.status_code, status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) - self.assertTrue(mocked_data.items() <= response.data.items()) + response = self.noAuthClient.post(path=url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(Token.objects.filter(user=self.existing_user).exists()) + + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_non_existing_user_get_userinfo(self, id_token_mock): - def test_existing_user_can_access_googleapis(self): url = self.BASE_URL data = { - "token": "existingusertoken123" + "id_token": "nonexistingusertoken123" } - response, mocked_res = self._google_view_response(url, data) + mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE + mock_return_value['email'] = 'nonexistinguser@tbcare.com' + id_token_mock.return_value = mock_return_value - mocked_data = json.loads(mocked_res.content) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(mocked_data.items() <= response.data.items()) + response = self.noAuthClient.post(path=url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) + self.assertFalse(Token.objects.filter(user=self.existing_user).exists()) + + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_not_valid_iss_token_should_fail(self, id_token_mock): - def test_bad_token_cannot_access_googleapis(self): url = self.BASE_URL data = { - "token": "somebadtoken123" + "id_token": "nonexistingusertoken123" } - response, mocked_res = self._google_view_response(url, data) + mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE + mock_return_value['iss'] = 'https://accounts.fake-google.com' + id_token_mock.return_value = mock_return_value - mocked_data = json.loads(mocked_res.content) + response = self.noAuthClient.post(path=url, data=data, format="json") self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.data['error'], mocked_data['error']) - def test_existing_user_get_auth_token(self): + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_not_valid_token_should_fail(self, id_token_mock): + url = self.BASE_URL data = { - "token": "existingusertoken123" + "id_token": "nonexistingusertoken123" } - response, mocked_res = self._google_view_response(url, data) + id_token_mock.side_effect = ValueError() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(Token.objects.filter(user=self.existing_user).exists()) + response = self.noAuthClient.post(path=url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) -- GitLab From ca959e9bb63e171241480534f46822b7c8a50416 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:21:03 +0700 Subject: [PATCH 39/43] [GREEN] Google OAuth2, using google-api-python-client instead of manually request to google --- apps/google/views.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/google/views.py b/apps/google/views.py index 6fdfb82..09d075b 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -1,35 +1,42 @@ from django.shortcuts import render from django.contrib.auth.models import User -from django.contrib.auth.base_user import BaseUserManager -from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.authtoken.models import Token -from apps.google.constants import GOOGLE_API_URL from apps.accounts.models import Account +from google.oauth2 import id_token +from google.auth.transport import requests as google_requests +from django.conf import settings import json -import requests class GoogleView(APIView): def post(self, request): - token = request.data.get('token') - res = requests.get(GOOGLE_API_URL + f"?access_token={token}") - if ('error' in res.text): - return Response(json.loads(res.text), status=status.HTTP_401_UNAUTHORIZED) - data = json.loads(res.text) + + token = request.data.get('id_token') + + try: + idinfo = id_token.verify_oauth2_token( + token, + google_requests.Request(), + settings.GOOGLE_CLIENT_ID + ) + + if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: + raise ValueError('Wrong issuer.') + except ValueError as err: + content = {'message': 'Invalid token'} + return Response(content, status=status.HTTP_401_UNAUTHORIZED) try: - user = User.objects.get(username=data['email']) + user = User.objects.get(username=idinfo['email']) accounts = Account.objects.get(user=user) except (User.DoesNotExist, Account.DoesNotExist): - return Response(data, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) + return Response(idinfo, status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION) token, _ = Token.objects.get_or_create(user=user) - response = {**data, "token": token.key} - + response = {**idinfo, "token": token.key} return Response(response, status=status.HTTP_200_OK) - -- GitLab From ca0ff37ccb48583600ffa899598e02335c8fccd9 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:21:43 +0700 Subject: [PATCH 40/43] [RED] Create Google OAuth2 User, using google-api-python-client instead of manually request to google --- .../tests/test_units/test_accounts.py | 116 ++++++++++-------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index 26d42e6..d46a6c4 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -28,36 +28,6 @@ import json import requests -@all_requests -def googleapis_mock(url, request): - if ('somebadtoken123' in url.query): - return response(401, { - "error": { - "code": 401, - "message": "Request is missing required authentication credential." + - " Expected OAuth 2 access token, login cookie or other valid" + - " authentication credential." + - " See https://developers.google.com/identity/sign-in/web/devconsole-project.", - "status": "UNAUTHENTICATED" - } - }) - if ('existingusertoken123' in url.query): - return response(200, { - "id": "existing_user", - "name": "Existing User", - "email": "existinguser@tbcare.com", - "verified_email": True, - "picture": "somepathtopicture", - }, headers={'Content-Type': 'application/json'}) - return response(200, { - "id": "somerandomid", - "name": "New User", - "email": "newuser@tbcare.com", - "verified_email": True, - "picture": "somepathtopicture", - }, headers={'Content-Type': 'application/json'}) - - class AccountViewTest(APITestCase): @classmethod def setUpTestData(cls): @@ -82,19 +52,29 @@ class AccountViewTest(APITestCase): cls.accounts = [cls.admin, cls.officer, cls.existing_google_officer] + cls.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE = { + "iss": "https://accounts.google.com", + "azp": "valid-client-id.apps.googleusercontent.com", + "aud": "valid-client-id.apps.googleusercontent.com", + "sub": "randomsub", + "email": "nonexistinguser@tbcare.com", + "email_verified": True, + "at_hash": "randomhash", + "name": "Random User", + "picture": "profile-image-url", + "given_name": "Random", + "family_name": "User", + "locale": "en-GB", + "iat": 1622290189, + "exp": 1622293789, + } + cls.faker = Faker() def setUp(self): self.noAuthClient = APIClient() self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token_1.key) - def _google_api_response(self, url, data): - with HTTMock(googleapis_mock): - response = self.noAuthClient.post(path=url, data=data, format="json") - mocked_res = requests.get( - f"{GOOGLE_API_URL}?access_token={data['token']}") - return response, mocked_res - def test_string_representation(self): admin_str = f"[Admin] {self.admin.user.username}" self.assertEqual(admin_str, str(self.admin)) @@ -314,73 +294,111 @@ class AccountViewTest(APITestCase): response = self.client.post(path=url, data=data, format="json",) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_google_create_account_success(self): + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_google_create_account_success(self, id_token_mock): url = self.BASE_URL + "google/" officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "token": "newusertoken123", + "id_token": "newusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, } - response, mocked_res = self._google_api_response(url, data) + id_token_mock.return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE + + response = self.noAuthClient.post(path=url, data=data, format="json") officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(officer_current_count, officer_prev_count + 1) - def test_google_create_account_with_existing_email_should_fail(self): + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_google_create_account_with_existing_email_should_fail(self, id_token_mock): url = self.BASE_URL + "google/" officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "token": "existingusertoken123", + "id_token": "existingusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, } - response, mocked_res = self._google_api_response(url, data) + mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE + mock_return_value['email'] = 'existinguser@tbcare.com' + id_token_mock.return_value = mock_return_value + + response = self.noAuthClient.post(path=url, data=data, format="json") officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) self.assertEqual(officer_current_count, officer_prev_count) - def test_google_create_account_with_inconsistent_district_and_sub_district_value(self): + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_google_create_account_with_inconsistent_district_and_sub_district_value( + self, id_token_mock): url = self.BASE_URL + "google/" officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "token": "newusertoken123", + "id_token": "newusertoken123", "phone_number": "+999999999999", "district": "Beji", "sub_district": "Limo", } - response, mocked_res = self._google_api_response(url, data) + mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE + mock_return_value['iss'] = 'https://accounts.fake-google.com' + id_token_mock.return_value = mock_return_value + + response = self.noAuthClient.post(path=url, data=data, format="json") officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(officer_current_count, officer_prev_count) - def test_google_create_account_with_bad_google_token(self): + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_google_create_account_with_bad_iss_in_token(self, id_token_mock): url = self.BASE_URL + "google/" officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "token": "somebadtoken123", + "id_token": "somebadtoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, } - response, mocked_res = self._google_api_response(url, data) + id_token_mock.side_effect = ValueError() + + response = self.noAuthClient.post(path=url, data=data, format="json") + officer_current_count = Account.objects.filter(is_admin=False).count() + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(officer_current_count, officer_prev_count) + + @patch('google.oauth2.id_token.verify_oauth2_token') + def test_google_create_account_with_bad_google_token(self, id_token_mock): + url = self.BASE_URL + "google/" + + officer_prev_count = Account.objects.filter(is_admin=False).count() + + data = { + "id_token": "somebadtoken123", + "phone_number": "+999999999999", + "district": self.officer.district, + "sub_district": self.officer.sub_district, + } + + id_token_mock.side_effect = ValueError() + + response = self.noAuthClient.post(path=url, data=data, format="json") officer_current_count = Account.objects.filter(is_admin=False).count() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) -- GitLab From f701dfe82077b5eeb04e324d50de6f7ee836f798 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:22:05 +0700 Subject: [PATCH 41/43] [GREEN] Create Google OAuth2 User, using google-api-python-client instead of manually request to google --- apps/accounts/serializers.py | 4 ++-- apps/accounts/views.py | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 78b4821..11d8264 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -117,13 +117,13 @@ class AccountGoogleSerializer(serializers.ModelSerializer): class AccountGoogleRegisterSerializer(serializers.ModelSerializer): - token = serializers.CharField() + id_token = serializers.CharField() class Meta: model = Account fields = [ "id", - "token", + "id_token", "phone_number", "district", "sub_district", diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 1e7e5b3..f1bdb22 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -9,7 +9,10 @@ from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated as DefaultIsAuthenticated +from google.oauth2 import id_token +from google.auth.transport import requests as google_requests from django.core.exceptions import ValidationError +from django.conf import settings from random import SystemRandom from string import ascii_uppercase, ascii_lowercase, digits @@ -45,10 +48,8 @@ from apps.constants import ( EMAIL_PASSWORD_RESET_CONTENT, EMAIL_PASSWORD_RESET_SUBJECT, ) -from apps.google.constants import GOOGLE_API_URL import json -import requests class AccountViewSet(viewsets.ModelViewSet): @@ -200,11 +201,19 @@ class AccountViewSet(viewsets.ModelViewSet): serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - token = serializer.validated_data.pop("token") - res = requests.get(GOOGLE_API_URL + f"?access_token={token}") - if ('error' in res.text): - return Response(json.loads(res.text), status=status.HTTP_400_BAD_REQUEST) - data = json.loads(res.text) + token = serializer.validated_data.pop("id_token") + try: + data = id_token.verify_oauth2_token( + token, + google_requests.Request(), + settings.GOOGLE_CLIENT_ID + ) + + if data['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: + raise ValueError('Wrong issuer.') + except ValueError as err: + content = {'message': 'Invalid token'} + return Response(content, status=status.HTTP_400_BAD_REQUEST) if User.objects.filter(username=data['email']).exists(): return Response( -- GitLab From 5baa82e6935866d47bde3122fb344d755008cb11 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 21:27:42 +0700 Subject: [PATCH 42/43] [GREEN] Create Google OAuth2 User, using google-api-python-client instead of manually request to google, removing unused import that causes test errors --- apps/accounts/tests/test_units/test_accounts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index d46a6c4..54aff33 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -7,7 +7,6 @@ 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 httmock import all_requests, HTTMock, response from apps.accounts.tests.factories.accounts import AccountFactory, UserFactory from apps.accounts.models import Account, AccountHistory -- GitLab From 779cdf4eb99a1ea2b7b8914e5b34797b3760d706 Mon Sep 17 00:00:00 2001 From: jahnsmichael Date: Sat, 29 May 2021 22:48:45 +0700 Subject: [PATCH 43/43] [CHORES] rename id_token to token for consistency with mobile --- apps/accounts/serializers.py | 4 ++-- apps/accounts/tests/test_units/test_accounts.py | 10 +++++----- apps/accounts/views.py | 2 +- apps/google/tests/test_units/test_google.py | 8 ++++---- apps/google/views.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 11d8264..78b4821 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -117,13 +117,13 @@ class AccountGoogleSerializer(serializers.ModelSerializer): class AccountGoogleRegisterSerializer(serializers.ModelSerializer): - id_token = serializers.CharField() + token = serializers.CharField() class Meta: model = Account fields = [ "id", - "id_token", + "token", "phone_number", "district", "sub_district", diff --git a/apps/accounts/tests/test_units/test_accounts.py b/apps/accounts/tests/test_units/test_accounts.py index 54aff33..dd1134b 100644 --- a/apps/accounts/tests/test_units/test_accounts.py +++ b/apps/accounts/tests/test_units/test_accounts.py @@ -300,7 +300,7 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "id_token": "newusertoken123", + "token": "newusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, @@ -321,7 +321,7 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "id_token": "existingusertoken123", + "token": "existingusertoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, @@ -345,7 +345,7 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "id_token": "newusertoken123", + "token": "newusertoken123", "phone_number": "+999999999999", "district": "Beji", "sub_district": "Limo", @@ -368,7 +368,7 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "id_token": "somebadtoken123", + "token": "somebadtoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, @@ -389,7 +389,7 @@ class AccountViewTest(APITestCase): officer_prev_count = Account.objects.filter(is_admin=False).count() data = { - "id_token": "somebadtoken123", + "token": "somebadtoken123", "phone_number": "+999999999999", "district": self.officer.district, "sub_district": self.officer.sub_district, diff --git a/apps/accounts/views.py b/apps/accounts/views.py index f1bdb22..3e6f640 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -201,7 +201,7 @@ class AccountViewSet(viewsets.ModelViewSet): serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - token = serializer.validated_data.pop("id_token") + token = serializer.validated_data.pop("token") try: data = id_token.verify_oauth2_token( token, diff --git a/apps/google/tests/test_units/test_google.py b/apps/google/tests/test_units/test_google.py index b2a67bb..5e33a5e 100644 --- a/apps/google/tests/test_units/test_google.py +++ b/apps/google/tests/test_units/test_google.py @@ -48,7 +48,7 @@ class GoogleViewTest(APITestCase): url = self.BASE_URL data = { - "id_token": "existingusertoken123" + "token": "existingusertoken123" } id_token_mock.return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE @@ -62,7 +62,7 @@ class GoogleViewTest(APITestCase): url = self.BASE_URL data = { - "id_token": "nonexistingusertoken123" + "token": "nonexistingusertoken123" } mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE @@ -78,7 +78,7 @@ class GoogleViewTest(APITestCase): url = self.BASE_URL data = { - "id_token": "nonexistingusertoken123" + "token": "nonexistingusertoken123" } mock_return_value = self.VERIFY_OAUTH2_TOKEN_MOCK_RESPONSE @@ -93,7 +93,7 @@ class GoogleViewTest(APITestCase): url = self.BASE_URL data = { - "id_token": "nonexistingusertoken123" + "token": "nonexistingusertoken123" } id_token_mock.side_effect = ValueError() diff --git a/apps/google/views.py b/apps/google/views.py index 09d075b..5600531 100644 --- a/apps/google/views.py +++ b/apps/google/views.py @@ -16,7 +16,7 @@ class GoogleView(APIView): def post(self, request): - token = request.data.get('id_token') + token = request.data.get('token') try: idinfo = id_token.verify_oauth2_token( -- GitLab