Django Rest Framework дает 302 в модульных тестах, когда force_login () на подробном представлении? - PullRequest
4 голосов
/ 16 января 2020

Я использую DJango Rest Framework для обслуживания API. У меня есть пара тестов, которые отлично работают. Чтобы сделать сообщение, пользователь должен войти в систему, и я также делаю некоторые проверки для подробного представления для вошедшего в систему пользователя. Я делаю это следующим образом:

class DeviceTestCase(APITestCase):
    USERNAME = "username"
    EMAIL = 'a@b.com'
    PASSWORD = "password"

    def setUp(self):
        self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
        self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
        self.sa_group.user_set.add(self.authorized_user)

    def test_post(self):
        device = DeviceFactory.build()
        url = reverse('device-list')

        self.client.force_login(self.authorized_user)
        response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
        self.client.logout()

        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
        # And some more tests here

    def test_detail_logged_in(self):
        device = DeviceFactory.create()

        url = reverse('device-detail', kwargs={'pk': device.pk})

        self.client.force_login(self.authorized_user)
        response = self.client.get(url)
        self.client.logout()

        self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
        # And some more tests here

Первый тест отлично работает. Он публикует новую запись и все проверки проходят. Второй тест не проходит, хотя. Выдает ошибку:

AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/

Оказывается, представление списка перенаправляет пользователя на экран входа в систему. Почему первый тест отлично регистрирует пользователя, но второй тест перенаправляет пользователя на экран входа? Я что-то упустил?

Вот вид:

class APIAuthGroup(InAuthGroup):
    """
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
    """
    allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]

    def has_permission(self, request, view):
        return request.method in SAFE_METHODS \
               or super(APIAuthGroup, self).has_permission(request, view)


class DevicesViewSet(DatapuntViewSetWritable):
    """
    A view that will return the devices and makes it possible to post new ones
    """

    queryset = Device.objects.all().order_by('id')

    serializer_class = DeviceSerializer
    serializer_detail_class = DeviceSerializer

    http_method_names = ['post', 'list', 'get']

    permission_classes = [APIAuthGroup]

Все советы приветствуются!

Ответы [ 4 ]

1 голос
/ 16 февраля 2020

Вот почему вы получаете эту ошибку.

Зависимые библиотеки

Я провел поиск по Именам классов, чтобы найти, какие библиотеки вы использовали, чтобы я мог воссоздать проблему на моей машине. Библиотека, вызывающая проблему, называется keycloak_idc. Эта библиотека устанавливает другую библиотеку mozilla_django_oidc, которая, по-видимому, и является причиной того, что вы получаете это.

Почему эта библиотека вызывает проблему

Внутри README файл этой библиотеки, он дает вам инструкции о том, как его настроить. Они находятся в этом файле. Внутри этих инструкций он дал указание добавить AUTHENTICATION_BACKENDS

AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]

. При добавлении этого бэкэнда аутентификации все ваши запросы проходят через промежуточное программное обеспечение, определенное внутри класса SessionRefresh, определенного внутри mozilla_django_oidc/middleware.py. Внутри этого класса всегда вызывается метод process_request().

Первое, что делает этот метод, вызывает метод is_refreshable_url(), который всегда возвращает False, если метод запроса был POST. В противном случае (когда метод запроса GET), он вернет True.

Теперь тело этого условия if было следующим:

        if not self.is_refreshable_url(request):
            LOGGER.debug('request is not refreshable')
            return
        # lots of stuff in here
        return HttpResponseRedirect(redirect_url)

Так как это промежуточное ПО, если запрос было POST, а возвращение было None, Django просто продолжит выполнение вашего запроса. Однако, когда запрос GET и вместо этого запускается строка return HttpResponseRedirect(redirect_url), Django даже не продолжит вызывать ваше представление и немедленно возвратит ответ 302.

Решение

После нескольких часов отладки я не вижу точной логики c в этом промежуточном программном обеспечении или что именно вы пытаетесь сделать, чтобы найти конкретное решение, поскольку все это началось с догадок, но наивно Исправить может быть то, что вы удалите AUTHENTICATION_BACKENDS из вашего файла настроек. Хотя я чувствую, что это неприемлемо, возможно, вы можете попробовать использовать другую библиотеку, которая выполняет то, что вы пытаетесь сделать, или найти альтернативный способ сделать это. Также, возможно, вы можете связаться с автором и посмотреть, что они думают.

1 голос
/ 10 февраля 2020

Итак, я думаю, что вы проверили это, и вы получите тот же результат:

class APIAuthGroup(InAuthGroup):
    def has_permission(self, request, view):
        return True

Почему вы используете DeviceFactory.build() в первом тесте и DeviceFactory.create() во втором?

Может быть, слияние двух может помочь вам:

def test_get(self):
    device = DeviceFactory.build()
    url = reverse('device-list')
    response = self.client.get(url)
    self.assertEqual(status.HTTP_200_OK, response.status_code)
0 голосов
/ 15 февраля 2020

Ваши рассуждения и код выглядят хорошо.

Несмотря на то, что вы не даете полный код, должна быть ошибка, которую вы не видите.

Подозрительный факт

  1. Это не значение по умолчанию 302, когда вы не вошли в систему. (@login_required, et c перенаправляет, но ваш код его нет)
  2. Ваше разрешение APIAuthGroup разрешает запросы GET для пользователя, не вошедшего в систему (return request.method in SAFE_METHODS), и вы используете запросы GET (self.client.get(url))

Таким образом, это означает, что вы не достигли конечной точки, которую, по вашему мнению, вы выполняете (ваш запрос на получение не соответствует методу DevicesViewSet)

Или это может быть случай, когда у вас есть global настройки разрешения / перенаправления в вашем settings.py, которые могут быть связаны с DRF .. например:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

Угадайте

url = reverse('device-detail', kwargs={'pk': device.pk}) 

, возможно, не указывает на URL вы думаете ... может быть, есть другой URL (/ sa / devices / 1 /), который переопределяет URL-адрес набора. (Возможно, у вас есть django URL-адрес на основе представления)

И я не спросил, почему вы получаете перенаправление после force_login.

Если это действительно перенаправление, связанное с входом в систему, все, о чем я могу думать, это self.authorized_user.refresh_from_db() или обновление request ..

Я полагаю, какое-либо свойство, связанное с входом в систему (например, сеанс или запрос). пользователь) может указывать на старый экземпляр .. (у меня нет никаких доказательств или факта, что это может произойти, но только догадка) и вам лучше не logging out/in для каждого теста)

0 голосов
/ 11 февраля 2020

Это проблема с методом setUp()? Из того, что я вижу, вы можете установить self.authorize_user для пользователя, который уже был создан в первом тесте.

Вместо этого я бы создал пользователя в каждом тесте, убедившись, что пользователь не существует уже, вот так:

user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
    self.authorize_user = User.objects.create_user....

Это объясняет, почему ваш первый тест прошел, почему ваш второй не прошел, и почему ответ @ anupam-chaplot не воспроизвел ошибку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...