Skip to content

Commit

Permalink
test google auth
Browse files Browse the repository at this point in the history
  • Loading branch information
jefer94 committed Sep 10, 2024
1 parent 0afe689 commit 73f5cfd
Show file tree
Hide file tree
Showing 14 changed files with 1,177 additions and 398 deletions.
4 changes: 4 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
black = "*"
capy-core = {extras = ["pytest"], version = "*"}
aioresponses = "*"
requests-mock = "*"

[packages]
django = "*"
Expand Down Expand Up @@ -147,3 +149,5 @@ google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
capy-core = {extras = ["django"], version = "*"}
google-api-python-client = "*"
python-dotenv = "*"
uvicorn-worker = "*"
1,198 changes: 815 additions & 383 deletions Pipfile.lock

Large diffs are not rendered by default.

176 changes: 176 additions & 0 deletions breathecode/authenticate/tests/urls/tests_google_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""
Test /v1/auth/subscribe
"""

from datetime import datetime, timedelta
import random
from unittest.mock import call

import pytest
from django.urls.base import reverse_lazy
from rest_framework import status

import capyc.pytest as capy
import staging.pytest as staging
from urllib.parse import quote


@pytest.fixture(autouse=True)
def setup(monkeypatch: pytest.MonkeyPatch, db):
monkeypatch.setenv("GOOGLE_CLIENT_ID", "123456.apps.googleusercontent.com")
monkeypatch.setenv("GOOGLE_SECRET", "123456")
monkeypatch.setenv("GOOGLE_REDIRECT_URL", "https://breathecode.herokuapp.com/v1/auth/google/callback")

yield


@pytest.fixture
def validation_res(patch_request):
validation_res = {
"quality_score": (random.random() * 0.4) + 0.6,
"email_quality": (random.random() * 0.4) + 0.6,
"is_valid_format": {
"value": True,
},
"is_mx_found": {
"value": True,
},
"is_smtp_valid": {
"value": True,
},
"is_catchall_email": {
"value": True,
},
"is_role_email": {
"value": True,
},
"is_disposable_email": {
"value": False,
},
"is_free_email": {
"value": True,
},
}
patch_request(
[
(
call(
"get",
"https://emailvalidation.abstractapi.com/v1/?api_key=None&[email protected]",
params=None,
timeout=10,
),
validation_res,
),
]
)
return validation_res


def test_no_token(database: capy.Database, client: capy.Client):
url = reverse_lazy("authenticate:google_callback")

response = client.get(url, format="json")

json = response.json()
expected = {"detail": "no-callback-url", "status_code": 400}

assert json == expected
assert response.status_code == status.HTTP_400_BAD_REQUEST

assert database.list_of("authenticate.Token") == []
assert database.list_of("authenticate.CredentialsGoogle") == []


def test_no_url(database: capy.Database, client: capy.Client):
url = reverse_lazy("authenticate:google_callback") + "?state=url%3Dhttps://4geeks.com"

response = client.get(url, format="json")

json = response.json()
expected = {"detail": "no-user-token", "status_code": 400}

assert json == expected
assert response.status_code == status.HTTP_400_BAD_REQUEST

assert database.list_of("authenticate.Token") == []
assert database.list_of("authenticate.CredentialsGoogle") == []


def test_no_code(database: capy.Database, client: capy.Client):
url = reverse_lazy("authenticate:google_callback") + "?state=token%3Dabc123%26url%3Dhttps://4geeks.com"

response = client.get(url, format="json")

json = response.json()
expected = {"detail": "no-code", "status_code": 400}

assert json == expected
assert response.status_code == status.HTTP_400_BAD_REQUEST

assert database.list_of("authenticate.Token") == []
assert database.list_of("authenticate.CredentialsGoogle") == []


def test_token_not_found(database: capy.Database, client: capy.Client):
url = (
reverse_lazy("authenticate:google_callback")
+ "?state=token%3Dabc123%26url%3Dhttps://4geeks.com&code=12345&scope=https://www.googleapis.com/auth/calendar.events"
)

response = client.get(url, format="json")

json = response.json()
expected = {"detail": "token-not-found", "status_code": 404}

assert json == expected
assert response.status_code == status.HTTP_404_NOT_FOUND

assert database.list_of("authenticate.Token") == []
assert database.list_of("authenticate.CredentialsGoogle") == []


def test_token(
database: capy.Database, client: capy.Client, format: capy.Format, utc_now: datetime, requests: staging.Requests
):
model = database.create(token={"token_type": "temporal"})
url = (
reverse_lazy("authenticate:google_callback")
+ f"?state=token%3D{model.token.key}%26url%3Dhttps://4geeks.com&code=12345&scope=https://www.googleapis.com/auth/calendar.events"
)

requests.post(
"https://oauth2.googleapis.com/token",
json={"access_token": "test_access_token", "expires_in": 3600, "refresh_token": "test_refresh_token"},
status_code=200,
)

response = client.get(url, format="json")

assert response.status_code == status.HTTP_302_FOUND
assert response.url == f"https://4geeks.com?token={quote(model.token.key)}"

assert requests.call_count == 1
last_request = requests.last_request

assert last_request.method == "POST"
assert last_request.url == "https://oauth2.googleapis.com/token"
assert last_request.qs == {}
assert last_request.json() == {
"client_id": "123456.apps.googleusercontent.com",
"client_secret": "123456",
"redirect_uri": "https://breathecode.herokuapp.com/v1/auth/google/callback",
"grant_type": "authorization_code",
"code": "12345",
}

assert database.list_of("authenticate.Token") == [format.to_obj_repr(model.token)]
assert database.list_of("authenticate.CredentialsGoogle") == [
{
"expires_at": utc_now + timedelta(seconds=3600),
"id": 1,
"refresh_token": "test_refresh_token",
"token": "test_access_token",
"user_id": 1,
},
]
88 changes: 88 additions & 0 deletions breathecode/authenticate/tests/urls/tests_google_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
Test /v1/auth/subscribe
"""

from typing import Any
from urllib.parse import urlencode

import pytest
from django.urls.base import reverse_lazy
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient

from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode
import capyc.pytest as capy

now = timezone.now()


@pytest.fixture(autouse=True)
def setup(monkeypatch: pytest.MonkeyPatch, db):
monkeypatch.setenv("GOOGLE_CLIENT_ID", "123456.apps.googleusercontent.com")
monkeypatch.setenv("GOOGLE_SECRET", "123456")
monkeypatch.setenv("GOOGLE_REDIRECT_URL", "https://breathecode.herokuapp.com/v1/auth/google/callback")

yield


def test_no_url(bc: Breathecode, client: APIClient):
url = reverse_lazy("authenticate:google_token", kwargs={"token": "78c9c2defd3be7f3f5b3ddd542ade55a2d35281b"})
response = client.get(url, format="json")

json = response.json()
expected = {"detail": "no-callback-url", "status_code": 400}

assert json == expected
assert response.status_code == status.HTTP_400_BAD_REQUEST


@pytest.mark.parametrize(
"token",
[
0,
{"token_type": "one_time"},
{"token_type": "permanent"},
{"token_type": "login"},
],
)
def test_invalid_token(database: capy.Database, client: capy.Client, token: Any):
key = "78c9c2defd3be7f3f5b3ddd542ade55a2d35281b"
model = database.create(token=token)
if "token" in model:
key = model.token.key

url = reverse_lazy("authenticate:google_token", kwargs={"token": key}) + "?url=https://4geeks.com"
response = client.get(url, format="json")

json = response.json()
expected = {"detail": "invalid-token", "status_code": 403}

assert json == expected
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.parametrize(
"token",
[
{"token_type": "temporal"},
],
)
def test_redirect(database: capy.Database, client: capy.Client, token: Any):
model = database.create(token=token)
callback_url = "https://4geeks.com/"

url = reverse_lazy("authenticate:google_token", kwargs={"token": model.token.key}) + f"?url={callback_url}"
response = client.get(url, format="json")

assert response.status_code == status.HTTP_302_FOUND
params = {
"response_type": "code",
"client_id": "123456.apps.googleusercontent.com",
"redirect_uri": "https://breathecode.herokuapp.com/v1/auth/google/callback",
"access_type": "offline",
"scope": "https://www.googleapis.com/auth/calendar.events",
"state": f"token={model.token.key}&url={callback_url}",
}

assert response.url == f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
28 changes: 15 additions & 13 deletions breathecode/authenticate/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2017,8 +2017,9 @@ def get_google_token(request, token=None):
except Exception:
pass

token = Token.get_valid(token) # IMPORTANT!! you can only connect to google with temporal short lasting tokens
if token is None or token.token_type != "temporal":
# you can only connect to google with temporal short lasting tokens
token = Token.get_valid(token, token_type="temporal")
if token is None:
raise ValidationException("Invalid or inactive token", code=403, slug="invalid-token")

params = {
Expand Down Expand Up @@ -2058,15 +2059,23 @@ def save_google_token(request):

state = parse_qs(request.query_params.get("state", None))

if state["url"] == None:
if state.get("url") == None:
raise ValidationException("No callback URL specified", slug="no-callback-url")
if state["token"] == None:

if state.get("token") == None:
raise ValidationException("No user token specified", slug="no-user-token")

code = request.query_params.get("code", None)
if code == None:
raise ValidationException("No google code specified", slug="no-code")

token = Token.get_valid(state["token"][0])
if not token:
logger.debug(f'Token {state["token"][0]} not found or is expired')
raise ValidationException(
"Token was not found or is expired, please use a different token", code=404, slug="token-not-found"
)

payload = {
"client_id": os.getenv("GOOGLE_CLIENT_ID", ""),
"client_secret": os.getenv("GOOGLE_SECRET", ""),
Expand All @@ -2075,9 +2084,8 @@ def save_google_token(request):
"code": code,
}
headers = {"Accept": "application/json"}
resp = requests.post("https://oauth2.googleapis.com/token", data=payload, headers=headers, timeout=2)
resp = requests.post("https://oauth2.googleapis.com/token", json=payload, headers=headers)
if resp.status_code == 200:

logger.debug("Google responded with 200")

body = resp.json()
Expand All @@ -2086,19 +2094,13 @@ def save_google_token(request):

logger.debug(body)

token = Token.get_valid(state["token"][0])
if not token:
logger.debug(f'Token {state["token"][0]} not found or is expired')
raise ValidationException(
"Token was not found or is expired, please use a different token", code=404, slug="token-not-found"
)

user = token.user
refresh = ""
if "refresh_token" in body:
refresh = body["refresh_token"]

CredentialsGoogle.objects.filter(user__id=user.id).delete()

google_credentials = CredentialsGoogle(
user=user,
token=body["access_token"],
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
os.environ["DATABASE_URL"] = "sqlite:///:memory:"

pytest_plugins = (
"staging.pytest.core",
"capyc.pytest.core",
"capyc.pytest.newrelic",
"capyc.pytest.django",
Expand Down
6 changes: 6 additions & 0 deletions docs/infrastructure/journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,9 @@ Side effects:
Reasons for the change:

- Web worker was reaching 841 MB ram.

## -9/09/2024

- `[all]` `GOOGLE_SECRET` setted.
- `[dev]` `GOOGLE_CLIENT_ID` setted.
- `[all]` `GOOGLE_REDIRECT_URL` setted.
4 changes: 2 additions & 2 deletions scripts/dyno/web.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/env bash

WEB_WORKER_CONNECTION=${WEB_WORKER_CONNECTION:-200}
WEB_WORKER_CLASS=${WEB_WORKER_CLASS:-uvicorn.workers.UvicornWorker}
WEB_WORKER_CLASS=${WEB_WORKER_CLASS:-uvicorn_worker.UvicornWorker}
CELERY_POOL=${CELERY_POOL:-prefork}
WEB_WORKERS=${WEB_WORKERS:-2}
WEB_TIMEOUT=${WEB_TIMEOUT:-29}
Expand All @@ -20,7 +20,7 @@ else
GUNICORN_PARAMS=""
fi

if [ "$WEB_WORKER_CLASS" = "uvicorn.workers.UvicornWorker" ]; then
if [ "$WEB_WORKER_CLASS" = "uvicorn_worker.UvicornWorker" ]; then
export SERVER_TYPE=asgi;
else
export SERVER_TYPE=wsgi;
Expand Down
Empty file added staging/core/__init__.py
Empty file.
Loading

0 comments on commit 73f5cfd

Please sign in to comment.