Skip to content
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
4 changes: 4 additions & 0 deletions djoser/social/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def validate(self, attrs):
user = backend.auth_complete()
except exceptions.AuthException as e:
raise serializers.ValidationError(str(e))
except (ConnectionError, OSError) as e:
raise serializers.ValidationError(
f"Network error during authentication: {str(e)}"
)
return {"user": user}

def _validate_state(self, value):
Expand Down
9 changes: 8 additions & 1 deletion djoser/social/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from social_django.utils import load_backend, load_strategy
from social_core.exceptions import MissingBackend

from djoser.conf import settings

Expand All @@ -20,7 +21,13 @@ def get(self, request, *args, **kwargs):
strategy.session_set("redirect_uri", redirect_uri)

backend_name = self.kwargs["provider"]
backend = load_backend(strategy, backend_name, redirect_uri=redirect_uri)
try:
backend = load_backend(strategy, backend_name, redirect_uri=redirect_uri)
except MissingBackend:
return Response(
{"detail": f"Provider '{backend_name}' not supported"},
status=status.HTTP_404_NOT_FOUND,
)

authorization_url = backend.auth_url()
return Response(data={"authorization_url": authorization_url})
52 changes: 51 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ tox = "^4.4.8"
babel = "^2.12.1"
pytest-mock = "^3.14.0"
deepdiff = "^8.0.1"
pytest-env = "^1.1.3"
factory-boy = "^3.3.0"

[tool.poetry.group.code-quality.dependencies]
black = ">=23.1,<26.0"
Expand Down Expand Up @@ -143,7 +145,11 @@ in-place = true
[tool.pytest.ini_options]
minversion = "7.0"
DJANGO_SETTINGS_MODULE = "testproject.settings"
python_paths = "testproject"
python_paths = ["testproject"]
addopts = "--tb=short --strict-markers"
markers = [
"django_db: mark test to use django database"
]

[tool.coverage.run]
source = ["djoser"]
Expand Down
146 changes: 143 additions & 3 deletions testproject/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,148 @@
import pytest
from rest_framework.test import APIClient
from djoser.conf import settings as djoser_settings

from testapp.factories import (
UserFactory,
TokenFactory,
)

Token = djoser_settings.TOKEN_MODEL


@pytest.fixture(autouse=True)
def allow_db_access(db):
yield


@pytest.fixture
def api_client():
"""DRF API client fixture."""
return APIClient()


@pytest.fixture
def user(db):
"""Create a basic user for testing."""
return UserFactory()


@pytest.fixture
def create_superuser(db):
"""Create a superuser for testing."""
return UserFactory.create(
username="admin",
email="admin@example.com",
is_superuser=True,
is_staff=True,
)


@pytest.fixture
def inactive_user(db):
"""Create an inactive user for testing."""
return UserFactory.create(is_active=False)


@pytest.fixture
def authenticated_client(api_client, user):
"""API client authenticated with a token."""
token = TokenFactory.create(user=user)
api_client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}")
return api_client


@pytest.fixture
def signal_tracker():
"""Track Django signals for testing."""

class SignalTracker:
def __init__(self):
self.signal_sent = False
self.signals_received = []

def receiver(self, sender=None, **kwargs):
self.signal_sent = True
self.signals_received.append({"sender": sender, "kwargs": kwargs})

def reset(self):
self.signal_sent = False
self.signals_received = []

return SignalTracker()


@pytest.fixture
def user():
from testapp.tests.common import create_user
def djoser_settings(settings):
"""
Fixture to easily modify DJOSER settings in tests.

Usage:
def test_something(djoser_settings):
djoser_settings["SEND_ACTIVATION_EMAIL"] = True
djoser_settings.update(WEBAUTHN={"RP_NAME": "test"})
# Settings are automatically applied
"""

class DjoserSettingsProxy:
def __init__(self, settings_dict):
# Make a copy to avoid modifying the original
self._settings = dict(settings_dict)
self._original_settings = dict(settings_dict)

def __getitem__(self, key):
return self._settings[key]

def __setitem__(self, key, value):
self._settings[key] = value
self._reload()

def __getattr__(self, name):
# Support attribute access like djoser_settings.EMAIL
if name.startswith("_"):
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
)
# First try to get from our custom settings
if name in self._settings:
return self._settings[name]
# If not found, check the actual djoser settings object
from djoser.conf import settings as djoser_conf_settings

return getattr(djoser_conf_settings, name, None)

def __setattr__(self, name, value):
if name.startswith("_"):
super().__setattr__(name, value)
else:
self._settings[name] = value
self._reload()

def update(self, **kwargs):
self._settings.update(kwargs)
self._reload()

def get(self, key, default=None):
return self._settings.get(key, default)

def clear(self):
# Reset to only default settings (not Django project settings)

self._settings = {}
self._reload()

def _reload(self):
# Update Django settings
settings.DJOSER = self._settings
# Force reload of djoser settings
from djoser.conf import reload_djoser_settings

reload_djoser_settings(setting="DJOSER", value=self._settings)

prx = DjoserSettingsProxy(settings.DJOSER)
yield prx
# Restore original settings
settings.DJOSER = prx._original_settings
from djoser.conf import reload_djoser_settings

return create_user()
reload_djoser_settings(setting="DJOSER", value=prx._original_settings)
73 changes: 73 additions & 0 deletions testproject/testapp/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import factory
from factory import Faker
from django.contrib.auth import get_user_model

from djoser.conf import settings as djoser_settings


User = get_user_model()


class BaseUserFactory(factory.django.DjangoModelFactory):
"""Base factory with common password handling logic."""

class Meta:
abstract = True
skip_postgeneration_save = True

@factory.post_generation
def password(self, create, extracted, **kwargs):
if create:
password_value = extracted if extracted is not None else "secret"
if password_value is None:
self.set_unusable_password()
self.raw_password = None
else:
self.set_password(password_value)
self.raw_password = password_value
self.save()


class UserFactory(BaseUserFactory):
class Meta:
model = User

username = factory.Sequence(lambda n: f"user{n}")
email = Faker("email")


class CustomUserFactory(BaseUserFactory):
class Meta:
model = "testapp.CustomUser"

custom_username = factory.Sequence(lambda n: f"user{n}")
custom_email = Faker("email")
custom_required_field = "42"


class ExampleUserFactory(BaseUserFactory):
class Meta:
model = "testapp.ExampleUser"

email = Faker("email")


class TokenFactory(factory.django.DjangoModelFactory):
class Meta:
model = djoser_settings.TOKEN_MODEL

user = factory.SubFactory(UserFactory)


class CredentialOptionsFactory(factory.django.DjangoModelFactory):
class Meta:
model = "webauthn.CredentialOptions"

challenge = Faker("sha256")
username = factory.Sequence(lambda n: f"user{n}")
display_name = Faker("name")
ukey = Faker("uuid4")
user = factory.SubFactory(UserFactory)
credential_id = Faker("sha256")
sign_count = 0
public_key = Faker("sha256")
Loading