Ruff9
Ruff9

Reputation: 1212

Django / Pytest / Splinter : IntegrityError duplicate key in test only

I know it's a very common problem and I read a lot of similar questions. But I can't find any solution, so, here I am with the 987th question on Stackoverflow about a Django Integrity error.

I'm starting a Django project with a Postgres db to learn about the framework. I did the classic Profile creation for users, automated with a post_save signal. Here is the model:

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    description = models.TextField(max_length=280, blank=True)
    contacts = models.ManyToManyField(
        "self",
        symmetrical=True,
        blank=True
    )

And this is the signal that goes with it :

def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()

post_save.connect(create_profile, sender=User, dispatch_uid="profile_creation")

The project is just starting, and for now I only create users in the admin view. With the post_save signal, it's supposed to create a Profile with the same form.

Here is the admin setup :

class ProfileInline(admin.StackedInline):
    model = Profile

class UserAdmin(admin.ModelAdmin):
    model = User
    list_display = ["username", "is_superuser"]
    fields = ["username", "is_superuser"]
    inlines = [ProfileInline]

I'm using pytest and Splinter for my test, and this is the integration test that don't work :

@pytest.mark.django_db
class TestAdminPage:
    def test_profile_creation_from_admin(self, browser, admin_user):
        browser.visit('/admin/login/')

        username_field = browser.find_by_css('form input[name="username"]')
        password_field = browser.find_by_css('form input[name="password"]')

        username_field.fill(admin_user.username)
        password_field.fill('password')

        submit = browser.find_by_css('form input[type="submit"]')
        submit.click()

        browser.links.find_by_text("Users").click()
        browser.links.find_by_partial_href("/user/add/").click()
        
        browser.find_by_css('form input[name="username"]').fill('Super_pseudo')
        browser.find_by_css('textarea[name="profile-0-description"]').fill('Super description')

        browser.find_by_css('input[name="_save"]').click()

        assert browser.url is '/admin/auth/user/'
        assert Profile.objects.last().description is 'Super description'

when I run this, I get this error :

django.db.utils.IntegrityError: duplicate key value violates unique constraint "profiles_profile_user_id_key"
DETAIL:  Key (user_id)=(2) already exists.

At first, I also saw this error when I was creating a user using my local server. But only if I wrote a description. If I let the description field empty, everything was working fine. So I wrote this integration test, to solve the issue. And then I read a lot, tweaked a few things, and the error stopped happening in my local browser. But not in my test suite.

So I used a breakpoint, there :

def create_profile(sender, instance, created, **kwargs):
    breakpoint()
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()

And that's where the fun begins. This single test is calling the signal 3 times.

The first time it's called by the admin_user fixture that I'm using.

(Pdb) from profiles.models import Profile
(Pdb) instance
<User: admin>
(Pdb) created
True
(Pdb) instance.profile
*** django.contrib.auth.models.User.profile.RelatedObjectDoesNotExist: User has no profile.
(Pdb) Profile.objects.count()
0
(Pdb) continue

Seems legit, the admin user don't have a profile, why not. Then the signal is called again on the same instance.

(Pdb) instance
<User: admin>
(Pdb) instance.profile
<Profile: admin>
(Pdb) Profile.objects.count()
1
(Pdb) Profile.objects.last()
<Profile: admin>
(Pdb) created
False
(Pdb) continue

Still legit, weird, but it's not doing anything. created is False, so it's not creating a second profile. Didn't need the first one, but it's not making the test fail. And then :

(Pdb) instance
<User: Super_pseudo>
(Pdb) created
True
(Pdb) instance.profile
<Profile: Super_pseudo>
(Pdb) Profile.objects.count()
1
(Pdb) Profile.objects.last()
<Profile: admin>

This is so weird. The profile is not saved, but it's raising an Integrity error anyway. It looks instanciated (why?), when I call instance.profile I get something (how?), but it don't look like it's saved in the db. But the error happens anyway. I have no clue, I spent a few hours already, and I don't know what to look.

Feels like I'm missing something important, and that's why I'm asking for your help.

Edit

I tried updating the signal with if created and not kwargs.get('raw', False):, but it doesn't work.

Just in case, the error message in full :

=================================== FAILURES ===================================
________________ TestAdminPage.test_profile_creation_from_admin ________________

self = <django.db.backends.utils.CursorWrapper object at 0x7f6f62521790>
sql = 'INSERT INTO "profiles_profile" ("user_id", "description") VALUES (%s, %s) RETURNING "profiles_profile"."id"'
params = (2, 'Super description')
ignored_wrapper_args = (False, {'connection': <DatabaseWrapper vendor='postgresql' alias='default'>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7f6f62521790>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "profiles_profile_user_id_key"
E               DETAIL:  Key (user_id)=(2) already exists.

/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py:89: UniqueViolation

The above exception was the direct cause of the following exception:

self = <profiles.tests.test_admin.TestAdminPage object at 0x7f6f62a543b0>
browser = <splinter.driver.djangoclient.DjangoClient object at 0x7f6f62822840>
admin_user = <User: admin>

    def test_profile_creation_from_admin(self, browser, admin_user):
        self.login_as_admin(browser, admin_user)
    
        browser.links.find_by_text("Users").click()
        browser.links.find_by_partial_href("/user/add/").click()
    
        browser.find_by_css('form input[name="username"]').fill('Super_pseudo')
        browser.find_by_css('textarea[name="profile-0-description"]').fill('Super description')
    
>       browser.find_by_css('input[name="_save"]').click()

profiles/tests/test_admin.py:28: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.12/site-packages/splinter/driver/lxmldriver.py:433: in click
    return self.parent.submit_data(parent_form)
/usr/local/lib/python3.12/site-packages/splinter/driver/djangoclient.py:130: in submit_data
    return super(DjangoClient, self).submit(form).content
/usr/local/lib/python3.12/site-packages/splinter/driver/lxmldriver.py:89: in submit
    self._do_method(method, url, data=data)
/usr/local/lib/python3.12/site-packages/splinter/driver/djangoclient.py:118: in _do_method
    self._response = func_method(url, data=data, follow=True, **extra)
/usr/local/lib/python3.12/site-packages/django/test/client.py:948: in post
    response = super().post(
/usr/local/lib/python3.12/site-packages/django/test/client.py:482: in post
    return self.generic(
/usr/local/lib/python3.12/site-packages/django/test/client.py:609: in generic
    return self.request(**r)
/usr/local/lib/python3.12/site-packages/django/test/client.py:891: in request
    self.check_exception(response)
/usr/local/lib/python3.12/site-packages/django/test/client.py:738: in check_exception
    raise exc_value
/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py:55: in inner
    response = get_response(request)
/usr/local/lib/python3.12/site-packages/django/core/handlers/base.py:197: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
/usr/local/lib/python3.12/contextlib.py:81: in inner
    return func(*args, **kwds)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:688: in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/utils/decorators.py:134: in _wrapper_view
    response = view_func(request, *args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/views/decorators/cache.py:62: in _wrapper_view_func
    response = view_func(request, *args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/sites.py:242: in inner
    return view(request, *args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:1886: in add_view
    return self.changeform_view(request, None, form_url, extra_context)
/usr/local/lib/python3.12/site-packages/django/utils/decorators.py:46: in _wrapper
    return bound_method(*args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/utils/decorators.py:134: in _wrapper_view
    response = view_func(request, *args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:1747: in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:1799: in _changeform_view
    self.save_related(request, form, formsets, not add)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:1255: in save_related
    self.save_formset(request, form, formset, change=change)
/usr/local/lib/python3.12/site-packages/django/contrib/admin/options.py:1243: in save_formset
    formset.save()
/usr/local/lib/python3.12/site-packages/django/forms/models.py:784: in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
/usr/local/lib/python3.12/site-packages/django/forms/models.py:944: in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
/usr/local/lib/python3.12/site-packages/django/forms/models.py:1142: in save_new
    return super().save_new(form, commit=commit)
/usr/local/lib/python3.12/site-packages/django/forms/models.py:757: in save_new
    return form.save(commit=commit)
/usr/local/lib/python3.12/site-packages/django/forms/models.py:542: in save
    self.instance.save()
/usr/local/lib/python3.12/site-packages/django/db/models/base.py:814: in save
    self.save_base(
/usr/local/lib/python3.12/site-packages/django/db/models/base.py:877: in save_base
    updated = self._save_table(
/usr/local/lib/python3.12/site-packages/django/db/models/base.py:1020: in _save_table
    results = self._do_insert(
/usr/local/lib/python3.12/site-packages/django/db/models/base.py:1061: in _do_insert
    return manager._insert(
/usr/local/lib/python3.12/site-packages/django/db/models/manager.py:87: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
/usr/local/lib/python3.12/site-packages/django/db/models/query.py:1805: in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
/usr/local/lib/python3.12/site-packages/django/db/models/sql/compiler.py:1820: in execute_sql
    cursor.execute(sql, params)
/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py:67: in execute
    return self._execute_with_wrappers(
/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py:80: in _execute_with_wrappers
    return executor(sql, params, many, context)
/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py:84: in _execute
    with self.db.wrap_database_errors:
/usr/local/lib/python3.12/site-packages/django/db/utils.py:91: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7f6f62521790>
sql = 'INSERT INTO "profiles_profile" ("user_id", "description") VALUES (%s, %s) RETURNING "profiles_profile"."id"'
params = (2, 'Super description')
ignored_wrapper_args = (False, {'connection': <DatabaseWrapper vendor='postgresql' alias='default'>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7f6f62521790>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                # params default might be backend specific.
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               django.db.utils.IntegrityError: duplicate key value violates unique constraint "profiles_profile_user_id_key"
E               DETAIL:  Key (user_id)=(2) already exists.

/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py:89: IntegrityError

Upvotes: 1

Views: 233

Answers (0)

Related Questions