Testing Django Admin Action (redirecting/auth issue) - django

I'm trying to write tests for an Admin action in the change_list view. I referred to this question but couldn't get the test to work. Here's my code and issue:
class StatusChangeTestCase(TestCase):
"""
Test case for batch changing 'status' to 'Show' or 'Hide'
"""
def setUp(self):
self.categories = factories.CategoryFactory.create_batch(5)
def test_status_hide(self):
"""
Test changing all Category instances to 'Hide'
"""
# Set Queryset to be hidden
to_be_hidden = models.Category.objects.values_list('pk', flat=True)
# Set POST data to be passed to changelist url
data = {
'action': 'change_to_hide',
'_selected_action': to_be_hidden
}
# Set change_url
change_url = self.reverse('admin:product_category_changelist')
# POST data to change_url
response = self.post(change_url, data, follow=True)
self.assertEqual(
models.Category.objects.filter(status='show').count(), 0
)
def tearDown(self):
models.Category.objects.all().delete()
I tried using print to see what the response was and this is what I got:
<HttpResponseRedirect status_code=302, "text/html; charset=utf-8", url="/admin/login/?next=/admin/product/category/">
It seems like it needs my login credentials - I tried to create a user in setUp() and log in as per Django docs on testing but it didn't seem to work.
Any help would be appreciated!

I found the solution - I wasn't instantiating Django's Client() class when I created a superuser, so whenever I logged in - it didn't persist in my subsequent requests. The correct code should look like this.
def test_status_hide(self):
"""
Test changing all Category instances to 'Hide'
"""
# Create user
user = User.objects.create_superuser(
username='new_user', email='test#example.com', password='password',
)
# Log in
self.client = Client()
self.client.login(username='new_user', password='password')
# Set Queryset to be hidden
to_be_hidden = models.Category.objects.values_list('pk', flat=True)
# Set POST data to be passed to changelist url
data = {
'action': 'change_to_hide',
'_selected_action': to_be_hidden
}
# Set change_url
change_url = self.reverse('admin:product_category_changelist')
# POST data to change_url
response = self.client.post(change_url, data, follow=True)
self.assertEqual(
models.Category.objects.filter(status='show').count(), 0
)

Related

Django test view fails: (1) unable to logged in a new created user and (2) CBV create view test do not create object in test database

I have a Django project and I want to implent unit tests.
I want to test a class based view (CBV) name PatientCreate that need to be authentificated.
It is important to not that the sqlite test dabase is already populated with users (data migration).
In my PatientTestCase class, I start defining setup where is created a new superuser named 'test' and logged in the new created user.
Then, I test if user 'test' is logged in but test fails. If logged in a user already registered in test database (i.e. user named 'admin'), test success.
To test for PatientCreate CBV, I write a post request and test if number of patients increase by one. But this test fails, even if I logged in with 'admin'. It seems that patient is not created in test database.
Note that, response.status_code = 200.
I can not figure out where comes my issue.
project architecture
- core
- urls.py
- ecrf
- urls.py
- views.py
tests.py
class PatientTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_superuser(
username='test', password='test', email='test#test.fr')
# => print(self.user) return test so user is created
self.login_success = self.client.login(username='test', password='test')
def test_new_patient_is_created(self):
self.assertTrue(self.login_success) # => return False with 'test' but will success with 'admin'
patient_site = Site.objects.get(sit_ide=4)
number_of_patients_before = Patient.objects.count()
response = self.client.post(reverse('ecrf:patient_create'), {
'pat': 'TES001',
'pat_sit': patient_site
}, follow=True, HTTP_ACCEPT_LANGUAGE='fr')
number_of_patients_after = Patient.objects.count()
self.assertEqual(number_of_patients_after,
number_of_patients_before + 1) # => return False
ecrf/views
#method_decorator(login_required, name="dispatch")
class PatientCreate(SuccessMessageMixin, CreateView):
model = Patient
form_class = PatientForm
success_message = "Le patient a été créé."
def get_success_url(self):
return reverse("ecrf:index")
def form_valid(self, form):
form.instance.pat_sai_log = self.request.user.username
form.save()
return super().form_valid(form)
In fact, I realized that CBV testing is obviously different than FBV.
Below, my solution, hope this help and is correct:
class CBVCreateViewTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(username='test', password='test', email='test#test.fr')
self.data= {...}
def test_object_is_created(self):
condition_before = ...
request = RequestFactory().post(reverse('app_name:view_name'), self.data,
follow=True, HTTP_ACCEPT_LANGUAGE='fr')
# logged-in user
request.user = self.user
# call to CBV
response = CBVCreateView.as_view()(request)
condition_after = ...
# Test condition_before and condition_after
Dealing with logged-in problem with FBV is that creating superuser using create_superuser() function lead to password not being properly hashed in unitest and I do not know why.
But, testing password of user created in setup method with check_password() method fails. But if I create user using create() method, and set user password 'manually' using set_password() method, it works!

How to plug the reponse of a django view GET request into the same view as a POST request during testing?

I have a Django function-based form view that initializes a form with default data on a GET request, and saves the model object on a POST request:
def copy(request, ann_id):
new_ann = get_object_or_404(Announcement, pk=ann_id)
new_ann.pk = None # autogen a new primary key (quest_id by default)
new_ann.title = "Copy of " + new_ann.title
new_ann.draft = True
new_ann.datetime_released = new_ann.datetime_released + timedelta(days=7)
form = AnnouncementForm(request.POST or None, instance=new_ann)
if form.is_valid():
new_announcement = form.save(commit=False)
new_announcement.author = request.user
new_announcement.datetime_created = timezone.now()
new_announcement.save()
form.save()
return redirect(new_announcement)
context = {
"title": "",
"heading": "Copy an Announcement",
"form": form,
"submit_btn_value": "Create",
}
return render(request, "announcements/form.html", context)
I can't figure out how to test the form.is_valid() branch when the form is posted, without manually providing the form data to self.client.post(url, form_data) in my view.
Here' what I'm trying:
test_views.py
class AnnouncementViewTests(TestCase):
def setUp(self):
self.client = ...
... etc
def test_copy_announcement(self):
# log in a teacher
success = self.client.login(username=self.test_teacher.username, password=self.test_password)
self.assertTrue(success)
# hit the view as a get request first, to load a copy of the announcement in the form
response = self.client.get(
reverse('announcements:copy', args=[self.test_announcement.id]),
)
self.assertEqual(response.status_code, 200)
# The form in this response should be valid, and should bw
# accepted on a post request,
# that is what I am testing in this integration test.
form_data = response.how_do_get_the_form_data() # ???????
response = self.client.post(
reverse('announcements:copy', args=[self.test_announcement.id]),
data=form_data
)
# Get the newest announcement just made in the post request
new_ann = Announcement.objects.latest('datetime_created')
self.assertRedirects(
response,
new_ann.get_absolute_url()
)
What I want to actually test is that the result of the get provides valid default data for the form that can then be submitted via post request.
But I can't figure out how to access the form data resulting from the get request, so I can then feed it into the form_data provided to the post request.
EDIT
I found the location of the form in the get response, but I have no idea how to get that in code.
You can access the response form in this way:
response.context['form']
From here you can build your payload in this way:
retrieved_instance = response.context['form'].instance
form_data = dict(title=retrieved_instance.title, ... <all the other fields>)
response = self.client.post(
reverse('announcements:copy', args=[self.test_announcement.id]),
data=form_data)
)
This is not like resubmitting the page but is very similar because you're resubmitting the same form.
Actually, your test seems more like an e2e test (when speaking about integrations and e2e there is some ambiguity let's say), for this reason, if I were you, I would switch "tool" and use selenium to simulate a user interaction from the beginning (open an existing announcement) to the end, pressing the submit button on your web page.
Only in this way what you're submitting is the "real" response of the "get"
If you are new to this kind of test, you can find here a simple tutorial https://lincolnloop.com/blog/introduction-django-selenium-testing/ to understand the main concepts.
Your question is bit confusing but I will try to head you in right direction.
To clean-up the code you should use Class Based View where you can use your form easily anywhere. A sample code I just wrote:
class TestView(View):
template_name = 'index.html'
success_url = 'home' # dummy view
context = {"form": myform()}
# myform is the definition/class of your form which contains all attrs.
def get(self, request):
context = self.context
context['form'] = form # fill out your data here for get request
return render(request, self.template_name, context)
def post(self, request):
context=self.context
# self.context certain that you're using exact form which you defined in class-scope
form=context['form']
# Form Validation
if form.is_valid():
#perform any logical validations here and save the form if required
return redirect(self.success_url)
context = self.context
context['form'] = form # just to show you that you can access that form anywhere
return render(request, self.template_name, context)
You coul manually pass data to you form like this and test the is_valid function in a unit test:
form_data = {'my': 'value', 'form': 'value', 'fields': 'value'}
form = AnnouncementForm(form_data)
self.assertFalse(form.is_valid)

Django model_mommy model instance isn't saved in Ajax test

I'm trying to test an django ajax view that saves a field. Here's the test:
def test_ajax_save_draft(self):
self.client.force_login(self.test_user) # view requires login
sub = mommy.make(QuestSubmission, quest=self.quest2)
draft_comment = "Test draft comment"
# Send some draft data via the ajax view, which should save it.
ajax_data = {
'comment': draft_comment,
'submission_id': sub.id,
}
self.client.post(
reverse('quests:ajax_save_draft'),
data=ajax_data,
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
self.assertEqual(draft_comment, sub.draft_text) # sub.draft_text = None, as if the ajax view was never called!
And here's the view:
#login_required
def ajax_save_draft(request):
if request.is_ajax() and request.POST:
submission_comment = request.POST.get('comment')
submission_id = request.POST.get('submission_id')
sub = get_object_or_404(QuestSubmission, pk=submission_id)
sub.draft_text = submission_comment
sub.save()
response_data = {}
response_data['result'] = 'Draft saved'
return HttpResponse(
json.dumps(response_data),
content_type="application/json"
)
else:
raise Http404
When I run the test, I get into the if block, and it can retrieve the comment and submission object, but when it returns to the test at the end, it's like the save never happened.
What am I doing wrong here?
Try calling sub.refresh_from_db()
Changes made to a model instance will only affect the instance being modified. Any existing model instances will not be updated, refresh_from_db will get the changes from the database

Django test with APIRequestFactory : how to pass "flat" parameter to a view

Django 2.2
I am writing tests for API using APIRequestFactory. The code that hits
/some_endpoint and /some_endpoint/<item_id> already works, and so does the test that tests /some_endpoint. However the test to test /some_endpoint/<item_id> does not work because I can not find a working way to pass that <item_id> value to the view code. Please not it's not /some_endpoint/<some_keyword>=<item_id> , it's "flat" in my case i.e. there's no keyword. The problem is <item_id> does not make it into the view code (it's always None in the classview in get_queryset method)
I tried to pass it as **kwargs, it does not arrive either ( see here). But that probably would not work anyway without keyword.
I tried to switch to use of Client instead of APIRequestFactory, same result. But I would rather get it working with APIRequestFactory unless it does not work this way in general. Below is the code.
test.py
def test_getByLongId(self) :
factory = APIRequestFactory()
item = Item.active.get(id=1)
print(item.longid)
#it prints correct longid here
request = factory.get("/item/%s" % item.longid)
view = ItemList.as_view()
force_authenticate(request, user=self.user)
response = view(request)
urls.py
urlpatterns = [
...
...
url(item/(?P<item_id>[a-zA-Z0-9-]+)/$', views.ItemList.as_view(), name='item-detail'),
...
...
]
views.py
class ItemList(generics.ListAPIView):
permission_classes = (IsBotOrReadOnly,)
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = ItemSerializer
schema = AutoSchema(
manual_fields=[
coreapi.Field("longid"),
]
)
def get_queryset(self):
"""
Optionally restricts the returned SampleSequencing to a given barcode.
"""
longid = self.kwargs.get('item_id', None)
print(longid)
#prints correct longid when executed by the webserver code and prints None when executed by the test
queryset = Item.active.filter(longid=longid)
return queryset
You have to pass item_id into the view():
def test_by_long_id(self) :
factory = APIRequestFactory()
item = Item.active.get(id=1)
print(item.longid)
#it prints correct longid here
request = factory.get("/item/%s" % item.longid)
view = ItemList.as_view()
force_authenticate(request, user=self.user)
response = view(request, item_id=item.longid)
or use APIClient:
from rest_framework.test import APIClient
# ...
#
def test_item_client(self):
item = Item.active.get(id=1)
client = APIClient()
url = '/item/%s/' % item.id
response = client.get(url)

Django REST Framework - unittest client failing to resolve hyperlinks relation for POST

I have this test:
class AttributeTest(APITestCase):
def setUp(self):
user1 = User.objects.create(pk=1, username='pepa', email='ads#asasd.cz', is_active=True, is_staff=True)
user1.set_password('mypass')
user1.save()
self.c1 = Campaign.objects.create(pk=1, owner=user1, project_name='c1')
def test(self):
campaign_url = 'http://testserver/api/campaigns/{}/'.format(self.c1.pk)
self.client.login(username='pepa', password='mypass')
data = {
"label": "something_here",
"parent_campaign": campaign_url,
}
# campaign clearly exists (created in setUp) and GET retrieve it:
assert self.client.get(campaign_url).json()['project_name'] == 'c1'
# I can even try it myself using pdb
# but this doesn't work - response return 400 Bad Request
# complaining about the very same hyperlink I can GET above
response = self.client.post('/api/keys', data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
but when run, it fails with {'parent_campaign': ['Invalid hyperlink - No URL match.']}.
When I try using curl or browsable API (outside the test environment), everything works as expected.
My serializer corresponding to the /api/keys:
class AttributeSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='api:key-detail')
parent_campaign = serializers.HyperlinkedRelatedField(
view_name='api:campaign-detail',
lookup_field='cid',
queryset=Campaign.objects.all())
def _get_user_campaigns(self):
user = self.context['view'].request.user
return Campaign.objects.filter(owner=user)
def get_fields(self, *args, **kwargs):
fields = super(AttributeSerializer, self).get_fields(*args, **kwargs)
fields['parent_campaign'].queryset = self._get_user_campaigns()
return fields
class Meta:
model = Key
fields = ("id", 'url', "label", 'parent_campaign')
Using serializer directly:
(Pdb) from api.attribute.serializers import AttributeSerializer
(Pdb) ser = AttributeSerializer(data=data)
(Pdb) ser.is_valid()
True
(Pdb) ser.save()
<Key: Something1 | MAROO | CID: lrvyw93>
Try reversing your url name and passing c1.pk as a url parameter, not just formatting it into your url:
from rest_framework.reverse import reverse
campaign_url_name = 'api:campaign-detail' # Use URL name instead of raw URL path
response = self.client.get(reverse(campaign_url_name, kwargs={'pk': self.c1.pk}))
I don't know why, but the results of tests had to be somehow cached. I restarted the PC and it worked with exactly the same commit. Solved.