2

I'm adding TokenAuthentication to our django project. All was going well, and I've added a migration and unit test for the token auth:

# Migration
from django.db import migrations


def create_missing_tokens(apps, schema_editor):
    """
    Tokens were added in 0002_auto_20160226_1747, we thus need to populate
    the tokens table for existing users
    """
    Token = apps.get_model('authtoken', 'Token')
    User = apps.get_model('accounts', 'CustomUser')
    for user in User.objects.all():
        Token.objects.get_or_create(user=user)


class Migration(migrations.Migration):

    dependencies = [
        # depends on authtoken migration
        ('accounts', '0003_subscription_max_updates_per_day'),
        ('authtoken', '0002_auto_20160226_1747'), # latest migration in the authtoken package
    ]
    operations = [
        migrations.RunPython(create_missing_tokens, reverse_code=migrations.RunPython.noop),
    ]

# unit test
class MigrationTestCase(TransactionTestCase):
    '''A Test case for testing migrations'''

    # These must be defined by subclasses.
    migrate_from = None
    migrate_to = None

    def setUp(self):
        super(MigrationTestCase, self).setUp()

        self.executor = MigrationExecutor(connection)
        self.executor.migrate(self.migrate_from)

    def migrate_to_dest(self):
        self.executor.loader.build_graph()  # reload.
        self.executor.migrate(self.migrate_to)

    @property
    def old_apps(self):
        return self.executor.loader.project_state(self.migrate_from).apps

    @property
    def new_apps(self):
        return self.executor.loader.project_state(self.migrate_to).apps

from accounts.models import CustomUserManager
class SummaryTestCase(MigrationTestCase):
    """
    We need to test that data is populated in the summary field on running the migration
    """

    migrate_from = [('accounts', '0003_subscription_max_updates_per_day')]
    migrate_to = [('accounts', '0004_create_tokens')]

    def setup_before_migration(self):
        manager = CustomUserManager()
        User = self.old_apps.get_model('accounts', 'CustomUser')
        manager.model = User
        manager.create_user(email='contact@a.fr',  # nosec
                            password='kjnfrkj',
)

    def test_token_populated(self):
        # runs setup
        self.setup_before_migration()

        # now migrate
        self.migrate_to_dest()

        # grab new models
        Token = self.new_apps.get_model('authtoken', 'Token')
        User = self.new_apps.get_model('accounts', 'CustomUser')
        for user in User.objects.all():
            self.assertTrue(Token.objects.filter(user_id=user.pk).exists())

This works great, but when i actually run the migration i get the message:

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

Here is some pseudo code for what i mean by "actually run the migration":

$ git checkout <old commit>          # grab old commit
$ ./run.sh go                        # spin up docker with server and db
$ git checkout master                # which includes migrations
$ ./run.sh again                     # log into docker image with django
$ (docker) python manage.py migrate  # run the migrations

the error i see is thus (full stack trace at the end of the question):

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

I cant understand how with a migration that uses Token.objects.get_or_create(user=user) I'm getting a duplicate key? any help would be greatly appreciated

  Applying accounts.0004_create_tokens...Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 538, in get_or_create
    return self.get(**kwargs), False
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 408, in get
    self.model._meta.object_name
__fake__.DoesNotExist: Token matching query does not exist.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "authtoken_token_pkey"
DETAIL:  Key (key)=() already exists.


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

Traceback (most recent call last):
  File "manage.py", line 15, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 83, in wrapped
    res = handle_func(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 234, in handle
    fake_initial=fake_initial,
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 117, in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 147, in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 245, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/operations/special.py", line 190, in database_forwards
    self.code(from_state.apps, schema_editor)
  File "/code/accounts/migrations/0004_create_tokens.py", line 12, in create_missing_tokens
    Token.objects.get_or_create(user=user)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 541, in get_or_create
    return self._create_object_from_params(kwargs, params)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 583, in _create_object_from_params
    raise e
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 575, in _create_object_from_params
    obj = self.create(**params)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 422, in create
    obj.save(force_insert=True, using=self.db)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 741, in save
    force_update=force_update, update_fields=update_fields)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 779, in save_base
    force_update, using, update_fields,
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 870, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 908, in _do_insert
    using=using, raw=raw)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 1186, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/sql/compiler.py", line 1335, in execute_sql
    cursor.execute(sql, params)
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 99, in execute
    return super().execute(sql, params)
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 76, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.6/site-packages/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib/python3.6/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.IntegrityError: duplicate key value violates unique constraint "authtoken_token_pkey"
DETAIL:  Key (key)=() already exists.

EDIT: Custom user class is nothing special, looks like this:

class CustomUser(AbstractUser):
    """
    Replace username by email as required and unique.
    """
    is_alphanumeric_or_dash = RegexValidator(r'^[0-9a-zA-Z\-]*$', 'Only alphanumeric and "-" characters are allowed.')

    # Hide username
    username = None

    # Overidde other fields
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'),
                                  max_length=100,
                                  blank=True,
                                  validators=[is_alphanumeric_or_dash])
    last_name = models.CharField(_('last name'),
                                 max_length=100,
                                 blank=True,
                                 validators=[is_alphanumeric_or_dash])

    # /!\ At some point, user should have a default subcription /!\
    subscription = models.ForeignKey(Subscription, on_delete=models.PROTECT, blank=True, null=True)
    # some other fields, but nothing special...
    USERNAME_FIELD = 'email'
    # Override the UserManager with our custom one (for objects creation)
    objects = CustomUserManager()
Preston
  • 7,399
  • 8
  • 54
  • 84
  • What does your `CustomUser` class look like? – Ben Nov 18 '19 at 19:48
  • I'd guess that your db somehow got horked while you were setting things up. Have you tried opening a shell and examining the existing `Token` objects? – RishiG Nov 18 '19 at 20:29
  • @Ben updated the question, thanks for the interest – Preston Nov 19 '19 at 12:42
  • I'm suspect that `username = None` is a problem. But I don't have any conclusive answers. I guess I would also look at things like: *1.* AUTH_USER_MODEL = 'accounts.CustomUser' , *2.* INSTALLED_APPS setup correctly , *3.* making sure `python manage.py drf_create_token ` works – Ben Nov 20 '19 at 17:38

1 Answers1

2

A Token's key is normally generated by its save() method. This is fine when you're generating the tokens manually but in a migration, where you're referencing the model via apps.get_model(), none of the custom model methods are available.

So what's happening is that the tokens are being generated with empty keys. The first one will work but all after that will generate this error because the key is not unique.

A simple workaround is to just copy the code that DRF uses to generate the key into your migration. Something like this should work:

for user in User.objects.using(db_alias).all():
    key = binascii.hexlify(os.urandom(20)).decode()
    Token.objects.using(db_alias).get_or_create(user=user, key=key)
StrongFish
  • 493
  • 7
  • 16