Repository: jazzband/django-oauth-toolkit

Issue:
Management command to create a Client Application
I am trying to use this library in my 1 API and 2 clients: 1 webclient and 1 android application.
I normally us automated CI/CD pipelines to deploy my scripts.

I am unable to easily auto-create the `Client Application` - https://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html#create-an-oauth2-client-application

It would be awesome if there is a management command to do this so I can just run that and create 2 clients using a secret/key I create during my deployment process


Discussion:

Resolving patch:
diff --git a/oauth2_provider/management/commands/createapplication.py b/oauth2_provider/management/commands/createapplication.py
new file mode 100644
--- /dev/null
+++ b/oauth2_provider/management/commands/createapplication.py
@@ -0,0 +1,85 @@
+from django.core.exceptions import ValidationError
+from django.core.management.base import BaseCommand
+
+from oauth2_provider.models import get_application_model
+
+Application = get_application_model()
+
+
+class Command(BaseCommand):
+    help = "Shortcut to create a new application in a programmatic way"
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            'client_type',
+            type=str,
+            help='The client type, can be confidential or public',
+        )
+        parser.add_argument(
+            'authorization_grant_type',
+            type=str,
+            help='The type of authorization grant to be used',
+        )
+        parser.add_argument(
+            '--client-id',
+            type=str,
+            help='The ID of the new application',
+        )
+        parser.add_argument(
+            '--user',
+            type=str,
+            help='The user the application belongs to',
+        )
+        parser.add_argument(
+            '--redirect-uris',
+            type=str,
+            help='The redirect URIs, this must be a space separated string e.g "URI1 URI2',
+        )
+        parser.add_argument(
+            '--client-secret',
+            type=str,
+            help='The secret for this application',
+        )
+        parser.add_argument(
+            '--name',
+            type=str,
+            help='The name this application',
+        )
+        parser.add_argument(
+            '--skip-authorization',
+            action='store_true',
+            help='The ID of the new application',
+        )
+
+    def handle(self, *args, **options):
+        # Extract all fields related to the application, this will work now and in the future
+        # and also with custom application models.
+        application_fields = [field.name for field in Application._meta.fields]
+        application_data = {}
+        for key, value in options.items():
+            # Data in options must be cleaned because there are unneded key-value like
+            # verbosity and others. Also do not pass any None to the Application
+            # instance so default values will be generated for those fields
+            if key in application_fields and value:
+                if key == 'user':
+                    application_data.update({'user_id': value})
+                else:
+                    application_data.update({key: value})
+
+        new_application = Application(**application_data)
+
+        try:
+            new_application.full_clean()
+        except ValidationError as exc:
+            errors = "\n ".join(['- ' + err_key + ': ' + str(err_value) for err_key,
+                                 err_value in exc.message_dict.items()])
+            self.stdout.write(
+                self.style.ERROR(
+                    'Please correct the following errors:\n %s' % errors
+                )
+            )
+        else:
+            new_application.save()
+            self.stdout.write(
+                self.style.SUCCESS('New application created successfully')
+            )


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
new file mode 100644
--- /dev/null
+++ b/tests/test_commands.py
@@ -0,0 +1,127 @@
+from io import StringIO
+
+from django.contrib.auth import get_user_model
+from django.core.management import call_command
+from django.core.management.base import CommandError
+from django.test import TestCase
+
+from oauth2_provider.models import get_application_model
+
+Application = get_application_model()
+
+
+class CreateApplicationTest(TestCase):
+
+    def test_command_creates_application(self):
+        output = StringIO()
+        self.assertEqual(Application.objects.count(), 0)
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            stdout=output,
+        )
+        self.assertEqual(Application.objects.count(), 1)
+        self.assertIn('New application created successfully', output.getvalue())
+
+    def test_missing_required_args(self):
+        self.assertEqual(Application.objects.count(), 0)
+        with self.assertRaises(CommandError) as ctx:
+            call_command(
+                'createapplication',
+                '--redirect-uris=http://example.com http://example2.com',
+            )
+
+        self.assertIn('client_type', ctx.exception.args[0])
+        self.assertIn('authorization_grant_type', ctx.exception.args[0])
+        self.assertEqual(Application.objects.count(), 0)
+
+    def test_command_creates_application_with_skipped_auth(self):
+        self.assertEqual(Application.objects.count(), 0)
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--skip-authorization',
+        )
+        app = Application.objects.get()
+
+        self.assertTrue(app.skip_authorization)
+
+    def test_application_created_normally_with_no_skipped_auth(self):
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+        )
+        app = Application.objects.get()
+
+        self.assertFalse(app.skip_authorization)
+
+    def test_application_created_with_name(self):
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--name=TEST',
+        )
+        app = Application.objects.get()
+
+        self.assertEqual(app.name, 'TEST')
+
+    def test_application_created_with_client_secret(self):
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--client-secret=SECRET',
+        )
+        app = Application.objects.get()
+
+        self.assertEqual(app.client_secret, 'SECRET')
+
+    def test_application_created_with_client_id(self):
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--client-id=someId',
+        )
+        app = Application.objects.get()
+
+        self.assertEqual(app.client_id, 'someId')
+
+    def test_application_created_with_user(self):
+        User = get_user_model()
+        user = User.objects.create()
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--user=%s' % user.pk,
+        )
+        app = Application.objects.get()
+
+        self.assertEqual(app.user, user)
+
+    def test_validation_failed_message(self):
+        output = StringIO()
+        call_command(
+            'createapplication',
+            'confidential',
+            'authorization-code',
+            '--redirect-uris=http://example.com http://example2.com',
+            '--user=783',
+            stdout=output,
+        )
+
+        self.assertIn('user', output.getvalue())
+        self.assertIn('783', output.getvalue())
+        self.assertIn('does not exist', output.getvalue())