Идея делать нормальный REST на Django – утопия, но некоторые моменты настолько логичные и нет одновременно, что об этом хочется писать. Ниже история про то, как мы сделали ViewSet от GenericViewSet и пары миксинов в DRF, покрыли это все тестами и получили местами странные, но абсолютно обоснованные коды ответов. Текст может быть полезен новичкам (или чуть более прошаренным) в Django, дабы уложить в голове формирование url’ов и порядок вызова методов permission-классов. Ну а бывалые скажут, что все это баловство и надо было использовать GenericApiView. Стандартная история любого веб приложения - CRUD для пользователя. Решили мы почему-то использовать для этих целей ViewSet, но ручки нужны были не все и чтобы лишнее не вытаскивать, взяли GenericViewSet и нужный Mixin.  Да, выбор странный, но история умалчивает о причинах такого решения, так что имеем что имеем. В итоге получили следующую картину: Все, что внутри класса нас пока не интересует, поэтому опустим этот момент.  Также у нас были вот такие пути: И захотелось нам проверить, что лишние ручки действительно недоступны (чтобы всякие там мимопроходилы их не трогали) и написать на это все дело тестов. Внимание вопрос: будет ли это работать?  Нет  Человеку, не сильно знакомому с DRF покажется, что наши тесты должны сработать. Но работать они не будут. А чтобы понять почему так происходит, нужно заглянуть в класс Router из DRF, который и формирует эту ошибку.  В этой части представлены исходники DRF, которые объясняют почему тесты падают и выдают не те http-статусы, которые ожидались. Если вам интересен конечный результат, можно пролистать сразу до следующего заголовка.  Причина AssertionError в тесте в том, как определены маршруты в классе Router. Если посмотреть на стандартный SimpleRouter из DRF увидим следующее (источник листинга): Что важно запомнить:  определен список из объектов Route в каждом объекте задаются: url, который будет сгенерирован  mapping - список из http-метода и соответствующего метода нашего ViewSet  Еще нам важно увидеть в этом классе следующий метод (источник листинга):  Здесь method_map это mapping из наших Route.  Получается, что для: Ну и наконец, если посмотреть на проверку в get_url все того же SimpleRouter, то увидим следующее (источник листинга): Из-за наследования от нашего класса от UpdateModelMixin SimpleRouter создал нам маршрут вида /users/:id, но разрешил там только http-методы PUT и PATCH. Но для DELETE используется тот же маршрут, но другой метод.  Поэтому первый тест на list будет стучаться на /users, который мы никак не определяли и будет получать в ответ 404, а вот второй тест на delete будет стучаться на существующий маршрут с несуществующим методом и получит в ответ 405. Работающие тесты будут выглядеть вот так: Спасибо, Django! Казалось бы: ну ладно, не совсем очевидно, но в принципе логично. Запомнили и разошлись. Но на этом история не закончилась и на том же проекте мы снова наткнулись на неожиданные статусы (хоть и вполне объяснимые). Определим еще один ViewSet для постов пользователя. Добавим ему permission-класс, который отвечает за то, можно ли мне как пользователю эти методы вызывать. Permission-класс должен отдать нам 403 код ошибки - доступ запрещен - когда мы попытаемся достать чужой пост. Но также мы хотим в списке показывать только посты пользователя, поэтому можем переопределить queryset - запрос, по которому достаются данные и доставать сразу с фильтром по пользователю. Теперь у нас во всех методах нашего ViewSet будут сразу данные пользователя и ничего лишнего. Но что произойдет если попытаться изменить чужую статью?  Ожидается, что 403. И если мы хотим покрыть это тестом, то он должен выглядеть как-то так: А как будет на самом деле? 404  А на самом деле все будет зависеть от того, какой метод определен в нашем permission-классе. А метода там два: has_permission - проверяет возможность действий в принципе; has_object_permission - проверяет возможность действий с конкретным объектом (в нашем случае - постом). Поскольку мы хотим изменить объект, то нужно определить get_object_permission. Тогда произойдет следующее: Django сначала выполнит get_queryset и от него попытается сделать .get() нашей записи, ничего не найдет и свалится в 404 так и не дойдя до проверки в permission-классе.  Но, если мы например, не авторизовались, то все-таки получим 403. Потому что проверка авторизации определена в has_permission. (Источник листинга) А has_permission выполняется до того как достается queryset. И снова спасибо, Django! Собираем воедино всё, о чем мы упоминали в тексте.  Порядок выполнения проверок примерно следующий: проверяем существует ли url в принципе - на этом этапе в случае ошибки будет 404; проверяем доступен ли http метод - здесь при неудаче будет 405; выполняем has_permission из permission_classes - тут 403; get_queryset из ViewSet - тут 404; проверяем has_object_permission - тут снова 403. Кстати, еще один забавный нюанс: если вы переопределяете методы retrieve, update, delete в своем ViewSet, то has_object_permission может и не вызваться. Подробнее здесь. Как говорится, ежики кололись, плакали, но продолжали жрать кактус пытаться сделать REST на Django.  Каких-то способов это обойти, кроме как не переопределять get_queryset или выкидывать нужные статусы в нужных ручках самостоятельно найдено не было. Надеемся, что кому-то этот текст сохранит пару нервных клеток при попытках понять, почему вместо 404 вы получили 403 или 405.  Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными статьями.  ИТ-Компания