Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/recaptcha verification #23

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ jobs:
env:
# Database for tests
DATABASE_URL: postgres://postgres:postgres@localhost/mobilityprofile
GOOGLE_RECAPTCHA_SECRET_KEY: testSecret
GOOGLE_RECAPTCHA_VERIFY_URL: https://www.google.com/recaptcha/api/siteverify


steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down
8 changes: 5 additions & 3 deletions account/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
from datetime import timedelta
from unittest.mock import patch as mock_patch

import pytest
from django.conf import settings
Expand Down Expand Up @@ -32,7 +33,6 @@ def test_token_expiration(api_client_authenticated, users, profiles):

@pytest.mark.django_db
def test_unauthenticated_cannot_do_anything(api_client, users):
# TODO, add start-poll url after recaptcha integration
urls = [
reverse("account:profiles-detail", args=[users.get(username="test1").id]),
]
Expand Down Expand Up @@ -66,9 +66,11 @@ def test_mailing_list_unsubscribe_throttling(


@pytest.mark.django_db
def test_profile_created(api_client):
@mock_patch("profiles.api.views.verify_recaptcha")
def test_profile_created(verify_recaptcha_mock, api_client):
verify_recaptcha_mock.return_value = True
url = reverse("profiles:question-start-poll")
response = api_client.post(url)
response = api_client.post(url, {"token": "foo"})
assert response.status_code == 200
assert User.objects.all().count() == 1
user = User.objects.first()
Expand Down
3 changes: 3 additions & 0 deletions config_dev.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ STATIC_URL=/static/

# Location of memcached
CACHE_LOCATION=127.0.0.1:11211

GOOGLE_RECAPTCHA_SECRET_KEY=Add your secret key
GOOGLE_RECAPTCHA_VERIFY_URL=https://www.google.com/recaptcha/api/siteverify
5 changes: 5 additions & 0 deletions mpbackend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
STATIC_URL=(str, "/static/"),
CORS_ORIGIN_WHITELIST=(list, []),
CACHE_LOCATION=(str, "127.0.0.1:11211"),
GOOGLE_RECAPTCHA_SECRET_KEY=(str, None),
GOOGLE_RECAPTCHA_VERIFY_URL=(str, None),
)
# WARN about env file not being preset. Here we pre-empt it.
env_file_path = os.path.join(BASE_DIR, CONFIG_FILE_NAME)
Expand Down Expand Up @@ -249,3 +251,6 @@
"LOCATION": env("CACHE_LOCATION"), # Address of the Memcached server
}
}

GOOGLE_RECAPTCHA_SECRET_KEY = env("GOOGLE_RECAPTCHA_SECRET_KEY")
GOOGLE_RECAPTCHA_VERIFY_URL = env("GOOGLE_RECAPTCHA_VERIFY_URL")
16 changes: 15 additions & 1 deletion profiles/api/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import uuid

import requests
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import IntegrityError, transaction
Expand Down Expand Up @@ -89,6 +90,12 @@ def sub_question_condition_met(sub_question_condition, user):
).exists()


def verify_recaptcha(token):
data = {"secret": settings.GOOGLE_RECAPTCHA_SECRET_KEY, "response": token}
response = requests.post(settings.GOOGLE_RECAPTCHA_VERIFY_URL, data=data)
return response.json().get("success", None)


def question_condition_met(question_condition_qs, user):
conditions_met = True
for question_condition in question_condition_qs:
Expand Down Expand Up @@ -187,7 +194,14 @@ def list(self, request, *args, **kwargs):
permission_classes=[AllowAny],
)
def start_poll(self, request):
# TODO check recaptha
try:
token = request.data["token"]
except KeyError:
return Response(status=status.HTTP_400_BAD_REQUEST)

if not verify_recaptcha(token):
return Response(status=status.HTTP_403_FORBIDDEN)

uuid4 = uuid.uuid4()
username = f"anonymous_{str(uuid4)}"
user = User.objects.create(pk=uuid4, username=username, is_generated=True)
Expand Down
10 changes: 0 additions & 10 deletions profiles/tests/api/test_answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse

from account.models import User
from profiles.models import Answer, Option, Question, SubQuestion


Expand All @@ -12,15 +11,6 @@ def test_answer_post_unauthenticated(api_client):
assert response.status_code == 401


@pytest.mark.django_db
def test_start_poll(api_client):
User.objects.all().count() == 0
url = reverse("profiles:question-start-poll")
response = api_client.post(url)
assert response.status_code == 200
assert User.objects.all().count() == 1


@pytest.mark.django_db
def test_post_answer(api_client_authenticated, users, questions, options):
user = users.get(username="test1")
Expand Down
16 changes: 12 additions & 4 deletions profiles/tests/api/test_postal_code_result.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

import pytest
from django.db.models import Sum
from rest_framework.authtoken.models import Token
Expand Down Expand Up @@ -155,7 +157,11 @@ def test_postal_code_result(

# post positive
for i in range(num_users):
response = api_client.post(start_poll_url)
response = None
with patch("profiles.api.views.verify_recaptcha") as mock_verify_recaptcha:
mock_verify_recaptcha.return_value = True
response = api_client.post(start_poll_url, {"token": "token"})

token = response.json()["token"]
assert response.status_code == 200
token = response.json()["token"]
Expand Down Expand Up @@ -213,9 +219,11 @@ def test_postal_code_result(

# post negative, but only to user Home postal code
for i in range(num_users):
response = api_client.post(start_poll_url)
token = response.json()["token"]
assert response.status_code == 200
with patch("profiles.api.views.verify_recaptcha") as mock_verify_recaptcha:
mock_verify_recaptcha.return_value = True
response = api_client.post(start_poll_url, {"token": "token"})
token = response.json()["token"]
assert response.status_code == 200
token = response.json()["token"]
user_id = response.json()["id"]
assert User.objects.all().count() == num_users + 1 + i
Expand Down
39 changes: 36 additions & 3 deletions profiles/tests/api/test_question.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import time
from unittest.mock import patch

import pytest
from django.conf import settings
from django.core.cache import cache
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse

from account.models import User
from profiles.models import Answer, PostalCodeResult


@pytest.mark.django_db
def test_poll_start(api_client):
with patch("profiles.api.views.verify_recaptcha") as mock_verify_recaptcha:
mock_verify_recaptcha.return_value = True
User.objects.all().count() == 0
url = reverse("profiles:question-start-poll")
response = api_client.post(url, {"token": "token"})
assert response.status_code == 200
User.objects.all().count() == 1


@pytest.mark.django_db
def test_poll_start_missing_token(api_client):
User.objects.all().count() == 0
url = reverse("profiles:question-start-poll")
response = api_client.post(url)
assert response.status_code == 400
User.objects.all().count() == 0


@pytest.mark.django_db
def test_poll_start_not_valid_token(api_client):
User.objects.all().count() == 0
url = reverse("profiles:question-start-poll")
response = api_client.post(url, {"token": "not valid"})
assert response.status_code == 403
User.objects.all().count() == 0


@pytest.mark.django_db
def test_sub_questions_conditions_states(
api_client,
Expand Down Expand Up @@ -426,9 +457,11 @@ def test_result_count_result_can_be_used_is_false(
def test_sub_question_condition(
api_client_authenticated, questions, sub_question_conditions, options, sub_questions
):
url = reverse("profiles:question-start-poll")
response = api_client_authenticated.post(url)
assert response.status_code == 200
with patch("profiles.api.views.verify_recaptcha") as mock_verify_recaptcha:
mock_verify_recaptcha.return_value = True
url = reverse("profiles:question-start-poll")
response = api_client_authenticated.post(url, {"token": "token"})
assert response.status_code == 200
answer_url = reverse("profiles:answer-list")
question_condition = questions.get(question="Do you use car?")
driving_question = questions.get(question="Questions about car driving")
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ django-cors-headers
freezegun
django-filter
pymemcache
requests
10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ black==23.3.0
# via -r requirements.in
build==0.10.0
# via pip-tools
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.3
# via
# black
Expand Down Expand Up @@ -53,6 +57,8 @@ flake8==6.0.0
# via -r requirements.in
freezegun==1.4.0
# via -r requirements.in
idna==3.6
# via requests
inflection==0.5.1
# via drf-spectacular
iniconfig==2.0.0
Expand Down Expand Up @@ -114,6 +120,8 @@ pytz==2023.3
# pandas
pyyaml==6.0
# via drf-spectacular
requests==2.31.0
# via -r requirements.in
six==1.16.0
# via python-dateutil
sqlparse==0.4.4
Expand All @@ -131,6 +139,8 @@ tzdata==2023.3
# via pandas
uritemplate==4.1.1
# via drf-spectacular
urllib3==2.2.1
# via requests
wheel==0.40.0
# via pip-tools

Expand Down
Loading