Django views' tests return 403/200 code instead of (presumably) 302
I'm testing views in my Django app. As the app is a backend of a forum site, I'm trying to test the creation, editing and deletion of a topic.
Creation, editing and deletion of a topic are implemented in my app to work via redirect:
- creation page (AddTopic) redirects to a succefully created topic's page;
- editing the topic's initial comment (UpdateFirstComment) redirects from the editing page to the edited topic's page;
- deletion page (DeleteTopic) redirects to a subforum (a chapter of a forum) where the deleted topic had belonged.
I presume (I'm not sure; and, most possibly, here is my mistake) that the successful redirect code is 302, and in the tests' assertion that's the code which should be checked. But in practice, creation and editing tests return code 200, while the deletion test returns code 403. And I, due to the lack of experience, hardly can explain why it happens this way and how to deal with it.
views.py:
class TopicListView(FilterView):
paginate_by = 20
model = Topic
template_name = "forum/subforum.html"
slug_url_kwarg = 'subforum_slug'
context_object_name = 'topics'
filterset_class = TopicFilter
def get_queryset(self):
qs = self.model.objects.all()
if self.kwargs.get('subforum_slug'):
qs = qs.filter(subforum__slug=self.kwargs['subforum_slug'])
return qs
class ShowTopic(DetailView):
model = Topic
template_name = "forum/topic.html"
slug_url_kwarg = 'topic_slug'
context_object_name = 'topic'
paginate_by = 5
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
topic = self.get_object()
comments_list = Comment.objects.filter(topic=topic).order_by('created')
paginator = Paginator(comments_list, self.paginate_by)
page_number = self.request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context.update({
'menu': menu,
'comments': page_obj,
'page_obj': page_obj,
'is_paginated': page_obj.has_other_pages(),
'paginator': paginator,
'comm_num': comments_list.count(),
#'topic_rep': topic.total_rep,
})
return context
class AddTopic(LoginRequiredMixin, CreateView):
form_class = AddTopicForm
template_name = 'forum/addtopic.html'
page_title = 'Create new topic'
def get_success_url(self):
return reverse('forum:topic', kwargs={
'subforum_slug': self.kwargs['subforum_slug'], 'topic_slug': self.object.slug})
def form_valid(self, form):
subforum = Subforum.objects.get(slug=self.kwargs['subforum_slug'])
form.instance.creator = self.request.user
form.instance.subforum = subforum
return super(AddTopic, self).form_valid(form)
class UpdateFirstComment(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Topic
form_class = AddTopicForm
template_name = 'forum/editcomment.html'
page_title = 'Edit topic'
def test_func(self):
topic = self.get_object()
if self.request.user == topic.creator or self.request.user.is_superuser:
return True
return False
def get_success_url(self):
return reverse('forum:topic', kwargs={
'subforum_slug': self.kwargs['subforum_slug'],
'topic_slug': self.kwargs['topic_slug']
})
def get_object(self, queryset=None):
return Topic.objects.get(slug=self.kwargs['topic_slug'], subforum__slug=self.kwargs['subforum_slug'])
def form_valid(self, form):
self.object = form.save(commit=False)
first_comment = self.object.first_comment
form.instance.creator = self.request.user
form.instance.topic = self.object
form.instance.first_comment = first_comment
return super(UpdateFirstComment, self).form_valid(form)
class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Topic
context_object_name = 'topic'
template_name = 'forum/topic_confirm_delete.html'
page_title = "Delete topic"
fields = '__all__'
def test_func(self):
if self.request.user.is_superuser:
return True
return False
def get_success_url(self):
return reverse('forum:subforum', kwargs={'subforum_slug': self.kwargs['subforum_slug']})
forum/urls.py (only for subforums and topics):
<...>
app_name = 'forum'
urlpatterns = [
path('', SubForumListView.as_view(), name='forum'),
path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
path('<slug:subforum_slug>/topics/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
path('<slug:subforum_slug>/topics/<slug:topic_slug>/edit_topic/', UpdateFirstComment.as_view(), name='edit_topic'),
path('<slug:subforum_slug>/topics/<slug:topic_slug>/delete_topic/', DeleteTopic.as_view(), name='delete_topic'),
]
tests.py (comments' section tests are omitted, they work fine):
from django.test import TestCase
from django.urls import reverse
from . import factories, models
from .models import Topic, Comment
class SubforumTestCase(TestCase):
def setUp(self):
self.subforum = factories.SubForumFactory()
self.user = factories.UserFactory()
self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
self.client.force_login(self.user)
def test_get_topic_list(self):
url = reverse('forum:subforum', kwargs={'subforum_slug': self.subforum.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['topics'].count(), models.Topic.objects.count())
def test_get_topic_detail(self):
url = reverse("forum:topic", kwargs={'subforum_slug': self.subforum.slug, 'topic_slug': self.topic.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "forum/topic.html")
def test_add_topic(self):
data = {
'subject': self.topic.subject,
'first_comment': self.topic.first_comment
}
url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
old_topics_count = Topic.objects.count()
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 302)
self.assertEqual(Topic.objects.count(), 2)
self.assertGreater(Topic.objects.count(), old_topics_count)
def test_update_first_comment(self):
data = {
'first_comment': "Chebuldyk"
}
url = reverse("forum:edit_topic", kwargs={
'subforum_slug': self.subforum.slug,
'topic_slug': self.topic.slug
})
old_first_comment = self.topic.first_comment
response = self.client.post(url, data=data)
self.topic.refresh_from_db()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(self.topic.first_comment, old_first_comment)
def test_delete_topic(self):
url = reverse("forum:delete_topic", kwargs={
'subforum_slug': self.topic.subforum.slug,
'topic_slug': self.topic.slug
})
old_topics_count = Topic.objects.count()
response = self.client.delete(url)
self.assertEqual(response.status_code, 302)
self.assertGreater(old_topics_count, Topic.objects.count())
results of testing:
Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...FF..F
======================================================================
FAIL: test_add_topic (forum.tests.SubforumTestCase.test_add_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 38, in test_add_topic
self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302
======================================================================
FAIL: test_delete_topic (forum.tests.SubforumTestCase.test_delete_topic)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\PyCharm Community Edition 2024.1.3\...\forum\tests.py", line 65, in test_delete_topic
self.assertEqual(response.status_code, 302)
AssertionError: 403 != 302
======================================================================
FAIL: test_update_first_comment (forum.tests.SubforumTestCase.test_update_first_comment)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\PyCharm Community Edition 2024.1.3\PycharmProjects\...\forum\tests.py", line 54, in test_update_first_comment
self.assertEqual(response.status_code, 302)
AssertionError: 200 != 302
----------------------------------------------------------------------
Ran 8 tests in 0.111s
FAILED (failures=3)
Tests work basing on factories; if necessary, I can provide them as well.
The response.status.code for add and update functions being 200 rather than 301 suggests you are getting errors, so you are not travelling to the success_url but staying on the page you are posting to.
Try adding a form_invalid function to print out errors to your terminal
def form_invalid(self, form):
response = super().form_invalid(form)
print form.errors
The 403 error fore delete_topic is because you are using self.client.delete
. While DELETE is a legitimate type of HTTP request, it's not commonly used for webpage interaction. You most likely want self.client.get (for a plain url or one with a querystring) or self.client.post (if you are posting form data) to communicate with the view that does the actual deletion of the model instance via code.
I've tried to delete the question, as I got the help with it on another site, but it doesn't allow me, so I'll post the answer to make it helpful for others:
- test_add_topic
print(response.context["form"].errors)
before asserts showed that the problem is that the topic is created with already existing subject. That reason is that the test for topic creation got the subject from already generated Faker topic.
Solution: to insert some original subject for a created topic:
def test_add_topic(self):
data = {
'subject': "Kai Cenat fanum tax", # here
'first_comment': self.topic.first_comment
}
url = reverse("forum:add_topic", kwargs={'subforum_slug': self.subforum.slug})
old_topics_count = Topic.objects.count()
response = self.client.post(url, data=data)
#print(response.context["form"].errors)
self.assertRedirects(
response,
f"/forum/{self.topic.subforum.slug}/topics/topic-{slugify(data['subject'])}/",
status_code=302,
target_status_code=200
)
self.assertEqual(response.status_code, 302)
self.assertEqual(Topic.objects.count(), 2)
self.assertGreater(Topic.objects.count(), old_topics_count)
- test_update_first_comment
My mistake. As subject is unique and requested field for topic, it must have been mentioned in data
.
Solution:
def test_update_first_comment(self):
data = {
'subject': self.topic.subject, # here
'first_comment': "Chebuldyk"
}
url = reverse("forum:edit_topic", kwargs={
'subforum_slug': self.subforum.slug,
'topic_slug': self.topic.slug
})
old_first_comment = self.topic.first_comment
response = self.client.post(url, data=data)
self.topic.refresh_from_db()
# print(response.context["form"].errors)
self.assertRedirects(
response,
f"/forum/{self.topic.subforum.slug}/topics/{self.topic.slug}/",
status_code=302,
target_status_code=200
)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(self.topic.first_comment, old_first_comment)
- test_delete_topic
Due to the limitations introduced by me, deletion of a topic requires admin rights. That's why the tests setUp required to insert another user of admin type. A corresponding Faker was created in factories.py.
After that the problem arose with AttributeError: Generic detail view DeleteTopic must be called with either an object pk or a slug in the URLconf.
, which for this time was connected with the corresponding view. DeleteView
has to either contain slug_url_kwarg
attribute defined, either the method get_object
overwritten to specify which URL to address.
Solution:
factories.py:
class UserFactory(factory.django.DjangoModelFactory):
username = factory.Faker('user_name')
password = factory.Faker('password')
email = factory.Faker('email')
class Meta:
model = User
class AdminFactory(UserFactory):
class Params:
superuser = factory.Trait(is_superuser=True, is_staff=True)
views.py DeleteTopic view:
class DeleteTopic(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Topic
context_object_name = 'topic'
slug_url_kwarg = 'topic_slug' # here
template_name = 'forum/topic_confirm_delete.html'
page_title = "Delete topic"
fields = '__all__'
def test_func(self):
if self.request.user.is_superuser:
return True
return False
'''
# or here; but in this separate case that's redundant
# Necessary when there are several parameters to make clear which slug to address
def get_object(self, queryset=None):
return Topic.objects.get(slug=self.kwargs['topic_slug'], subforum__slug=self.kwargs['subforum_slug'])
'''
def get_success_url(self):
return reverse('forum:subforum', kwargs={'subforum_slug': self.kwargs['subforum_slug']})
tests.py setUp and test_delete_topic:
def setUp(self):
self.subforum = factories.SubForumFactory()
self.user = factories.UserFactory()
self.super_admin = factories.AdminFactory(superuser=True) # here
self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
# self.client.force_login(self.user)
self.client.force_login(self.super_admin) # here
def test_delete_topic(self):
url = reverse("forum:delete_topic", kwargs={
'subforum_slug': self.subforum.slug,
'topic_slug': self.topic.slug
})
old_topics_count = Topic.objects.count()
response = self.client.delete(url)
# print(response)
# print(response.context["form"].errors)
self.assertRedirects(response, f"/forum/{self.subforum.slug}/", status_code=302, target_status_code=200)
self.assertEqual(response.status_code, 302)
self.assertGreater(old_topics_count, Topic.objects.count())
All hails to DjangoForum. Guys are saviours.