Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/breatheco-de/apiv2
Browse files Browse the repository at this point in the history
  • Loading branch information
alesanchezr committed Oct 29, 2024
2 parents acb24f2 + 02e904b commit ea561ad
Show file tree
Hide file tree
Showing 60 changed files with 3,808 additions and 2,074 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ SCREENSHOT_MACHINE_KEY=00000

# Email validation
MAILBOX_LAYER_KEY=adsasd
SET_GOOGLE_CREDENTIALS=1
Empty file added .flags
Empty file.
3,512 changes: 1,807 additions & 1,705 deletions Pipfile.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions breathecode/authenticate/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
GithubAcademyUser,
GithubAcademyUserLog,
GitpodUser,
GoogleWebhook,
Profile,
ProfileAcademy,
Role,
Expand Down Expand Up @@ -498,3 +499,11 @@ def authenticate(self, obj):
return format_html(
f"<a href='/v1/auth/github?user={obj.github_owner.id}&url={self.github_callback}&scope={scopes}'>connect owner</a>"
)


@admin.register(GoogleWebhook)
class GoogleWebhookAdmin(admin.ModelAdmin):
list_display = ("id", "type", "status", "status_text", "created_at", "updated_at")
search_fields = ["status", "status_text"]
list_filter = ("type", "status")
actions = []
1 change: 1 addition & 0 deletions breathecode/authenticate/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ class AcademyConfig(AppConfig):

def ready(self):
from . import receivers # noqa: F401
from . import flags # noqa: F401
16 changes: 16 additions & 0 deletions breathecode/authenticate/flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os

from capyc.core.managers import feature

flags = feature.flags


@feature.availability("authenticate.set_google_credentials")
def set_google_credentials() -> bool:
if os.getenv("SET_GOOGLE_CREDENTIALS") in feature.TRUE:
return True

return False


feature.add(set_google_credentials)
34 changes: 34 additions & 0 deletions breathecode/authenticate/migrations/0059_googlewebhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.1.2 on 2024-10-22 19:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authenticate", "0058_credentialsgoogle_google_id"),
]

operations = [
migrations.CreateModel(
name="GoogleWebhook",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"message",
models.SlugField(blank=True, help_text="base64 message provided by google", max_length=124),
),
(
"status",
models.CharField(
choices=[("PENDING", "Pending"), ("DONE", "Done"), ("ERROR", "Error")],
default="PENDING",
max_length=9,
),
),
("status_text", models.CharField(default="", max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-10-22 19:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authenticate", "0059_googlewebhook"),
]

operations = [
migrations.AlterField(
model_name="googlewebhook",
name="message",
field=models.SlugField(blank=True, help_text="base64 message provided by google", max_length=512),
),
]
18 changes: 18 additions & 0 deletions breathecode/authenticate/migrations/0061_googlewebhook_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-10-22 22:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authenticate", "0060_alter_googlewebhook_message"),
]

operations = [
migrations.AddField(
model_name="googlewebhook",
name="type",
field=models.CharField(default="noSet", max_length=40),
),
]
24 changes: 23 additions & 1 deletion breathecode/authenticate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
from breathecode.utils.validators import validate_language_code

from .signals import academy_invite_accepted
from .signals import academy_invite_accepted, google_webhook_saved

__all__ = [
"User",
Expand Down Expand Up @@ -704,3 +704,25 @@ def __init__(self, *args, **kwargs):
raise DeprecationWarning("authenticate.App was deprecated, use linked_services.App instead")

name = models.CharField(max_length=25, unique=True, help_text="Descriptive and unique name of the app")


class GoogleWebhook(models.Model):
class Status(models.TextChoices):
PENDING = ("PENDING", "Pending")
DONE = ("DONE", "Done")
ERROR = ("ERROR", "Error")

message = models.SlugField(max_length=512, blank=True, help_text="base64 message provided by google")
type = models.CharField(max_length=40, default="noSet")

status = models.CharField(max_length=9, choices=Status, default=Status.PENDING)
status_text = models.CharField(max_length=255, default="")

created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)

def save(self, *args, **kwargs):
created = self.pk is None
super().save(*args, **kwargs)
if created:
google_webhook_saved.send_robust(sender=self.__class__, instance=self, created=created)
2 changes: 2 additions & 0 deletions breathecode/authenticate/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class CohortTinySerializer(serpy.Serializer):
"""The serializer schema definition."""

# Use a Field subclass like IntField if you need more validation.
id = serpy.Field()
slug = serpy.Field()
name = serpy.Field()

Expand Down Expand Up @@ -1290,6 +1291,7 @@ class Meta:
"conversion_info",
"asset_slug",
"event_slug",
"has_marketing_consent",
)

def validate(self, data: dict[str, str]):
Expand Down
1 change: 1 addition & 0 deletions breathecode/authenticate/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
user_info_deleted = emisor.signal("user_info_deleted")

cohort_user_deleted = emisor.signal("cohort_user_deleted")
google_webhook_saved = emisor.signal("google_webhook_saved")
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ def setUp(self):
# the behavior of permissions is not exact, this changes every time you add a model
self.latest_content_type_id = content_type.id
self.latest_permission_id = permission.id
self.job_content_type_id = self.latest_content_type_id - 63
self.can_delete_job_permission_id = self.latest_permission_id - 253
self.job_content_type_id = self.latest_content_type_id - 64
self.can_delete_job_permission_id = self.latest_permission_id - 257

"""
🔽🔽🔽 format of PERMISSIONS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from unittest.mock import MagicMock, call

import capyc.pytest as capy
import pytest

from breathecode.notify import tasks
from breathecode.payments.tasks import process_google_webhook
from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode


@pytest.fixture(autouse=True)
def setup(db, monkeypatch):
monkeypatch.setattr("breathecode.payments.tasks.process_google_webhook.delay", MagicMock())
yield


def test_each_webhook_is_processed(database: capy.Database, signals: capy.Signals, format: capy.Format):
signals.enable("breathecode.authenticate.signals.google_webhook_saved")

model = database.create(
google_webhook=2,
)

assert database.list_of("authenticate.GoogleWebhook") == [
format.to_obj_repr(model.google_webhook[0]),
format.to_obj_repr(model.google_webhook[1]),
]

assert process_google_webhook.delay.call_args_list == [
call(1),
call(2),
]
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def test_1_invite(database: capy.Database, format: capy.Format):
assert logging.Logger.error.call_args_list == [
call("User not found for user invite 1", exc_info=True),
]
# assert 0


@pytest.mark.parametrize(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def generate_user_invite(self, model, user_invite, arguments={}):
"logo_url": model.academy.logo_url,
},
"cohort": {
"id": model.cohort.id,
"name": model.cohort.name,
"slug": model.cohort.slug,
},
Expand Down Expand Up @@ -362,6 +363,7 @@ def test_resend_invite_with_invitation(self):
},
"role": {"id": "potato", "name": "potato", "slug": "potato"},
"cohort": {
"id": model["cohort"].id,
"slug": model["cohort"].slug,
"name": model["cohort"].name,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def generate_user_invite(self, model, user_invite, arguments={}):
"logo_url": model.academy.logo_url,
},
"cohort": {
"id": model.cohort.id,
"name": model.cohort.name,
"slug": model.cohort.slug,
},
Expand Down
23 changes: 18 additions & 5 deletions breathecode/authenticate/tests/urls/tests_google_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@
Test /v1/auth/subscribe
"""

from datetime import datetime, timedelta
import random
from unittest.mock import call
from datetime import datetime, timedelta
from unittest.mock import MagicMock, call
from urllib.parse import quote

import capyc.pytest as capy
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")
monkeypatch.setattr("breathecode.services.google_apps.GoogleApps.__init__", MagicMock(return_value=None))
monkeypatch.setattr("breathecode.services.google_apps.GoogleApps.subscribe_meet_webhook", MagicMock())
monkeypatch.setattr(
"breathecode.services.google_apps.GoogleApps.get_user_info", MagicMock(return_value={"id": 123})
)

yield

Expand Down Expand Up @@ -152,7 +157,13 @@ def test_token(
json=payload,
headers={"Accept": "application/json"},
).response(
{"access_token": "test_access_token", "expires_in": 3600, "refresh_token": "test_refresh_token"}, status=200
{
"access_token": "test_access_token",
"expires_in": 3600,
"refresh_token": "test_refresh_token",
"id_token": "test_id_token",
},
status=200,
)

response = client.get(url, format="json")
Expand All @@ -167,6 +178,8 @@ def test_token(
{
"expires_at": utc_now + timedelta(seconds=3600),
"id": 1,
"google_id": "123",
"id_token": "test_id_token",
"refresh_token": "test_refresh_token",
"token": "test_access_token",
"user_id": 1,
Expand Down
54 changes: 50 additions & 4 deletions breathecode/authenticate/tests/urls/tests_google_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
"""

from typing import Any
from unittest.mock import MagicMock
from urllib.parse import urlencode

import capyc.pytest as capy
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()

Expand All @@ -22,6 +23,7 @@ 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")
monkeypatch.setenv("SET_GOOGLE_CREDENTIALS", "true")

yield

Expand Down Expand Up @@ -83,12 +85,56 @@ def test_redirect(database: capy.Database, client: capy.Client, token: Any):
"scope": " ".join(
[
"https://www.googleapis.com/auth/meetings.space.created",
# "https://www.googleapis.com/auth/meetings.space.readonly",
"https://www.googleapis.com/auth/drive.meet.readonly",
# "https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/userinfo.profile",
]
),
"state": f"token={model.token.key}&url={callback_url}&academysettings=none",
}

assert response.url == f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"


@pytest.mark.parametrize(
"token",
[
{"token_type": "temporal"},
],
)
@pytest.mark.parametrize(
"academy_settings",
[
"overwrite",
"set",
],
)
def test_redirect_with_academy_settings(
database: capy.Database, client: capy.Client, token: Any, academy_settings: str
):
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}&academysettings={academy_settings}"
)
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": " ".join(
[
"https://www.googleapis.com/auth/meetings.space.created",
"https://www.googleapis.com/auth/drive.meet.readonly",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/pubsub",
]
),
"state": f"token={model.token.key}&url={callback_url}",
"state": f"token={model.token.key}&url={callback_url}&academysettings={academy_settings}",
}

assert response.url == f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
Loading

0 comments on commit ea561ad

Please sign in to comment.