Skip to content

Commit

Permalink
Merge pull request #1456 from jefer94/feat/google-auth
Browse files Browse the repository at this point in the history
test google auth
  • Loading branch information
jefer94 authored Sep 11, 2024
2 parents 28c60d2 + 7183a7d commit 1d2d47c
Show file tree
Hide file tree
Showing 15 changed files with 1,200 additions and 624 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,5 @@ google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
capy-core = {extras = ["django"], version = "*"}
google-api-python-client = "*"
python-dotenv = "*"
uvicorn-worker = "*"
1,202 changes: 621 additions & 581 deletions Pipfile.lock

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions breathecode/authenticate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TryToGetOrCreateAOneTimeToken,
)
from breathecode.utils.validators import validate_language_code
from asgiref.sync import sync_to_async

from .signals import academy_invite_accepted

Expand Down Expand Up @@ -632,16 +633,21 @@ def get_or_create(cls, user, token_type: str, **kwargs: Unpack[TokenGetOrCreateA
return token, created

@classmethod
def get_valid(cls, token: str, **kwargs: Unpack[TokenFilterArgs]) -> "Token | None":
def get_valid(cls, token: str, async_mode: bool = False, **kwargs: Unpack[TokenFilterArgs]) -> "Token | None":
utc_now = timezone.now()
cls.delete_expired_tokens()

qs = Token.objects.filter(Q(expires_at__gt=utc_now) | Q(expires_at__isnull=True), key=token, **kwargs)
if async_mode:
qs = qs.prefetch_related("user")

# find among any non-expired token
return (
Token.objects.filter(key=token, **kwargs)
.filter(Q(expires_at__gt=utc_now) | Q(expires_at__isnull=True))
.first()
)
return qs.first()

@classmethod
@sync_to_async
def aget_valid(cls, token: str, **kwargs: Unpack[TokenFilterArgs]) -> "Token | None":
return cls.get_valid(token, async_mode=True, **kwargs)

@classmethod
def validate_and_destroy(cls, hash: str) -> User:
Expand Down
174 changes: 174 additions & 0 deletions breathecode/authenticate/tests/urls/tests_google_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
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, http: staging.HTTP
):
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"
)

payload = {
"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",
}

http.post(
"https://oauth2.googleapis.com/token",
json=payload,
headers={"Accept": "application/json"},
).response(
{"access_token": "test_access_token", "expires_in": 3600, "refresh_token": "test_refresh_token"}, status=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)}"

http.call_count == 1

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,
},
]
87 changes: 87 additions & 0 deletions breathecode/authenticate/tests/urls/tests_google_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
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": "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)}"
Loading

0 comments on commit 1d2d47c

Please sign in to comment.