diff options
Diffstat (limited to 'pydis_site/apps/api')
43 files changed, 682 insertions, 392 deletions
diff --git a/pydis_site/apps/api/__init__.py b/pydis_site/apps/api/__init__.py index e69de29b..afa5b4d5 100644 --- a/pydis_site/apps/api/__init__.py +++ b/pydis_site/apps/api/__init__.py @@ -0,0 +1 @@ +default_app_config = 'pydis_site.apps.api.apps.ApiConfig' diff --git a/pydis_site/apps/api/apps.py b/pydis_site/apps/api/apps.py index 76810b2e..18eda9e3 100644 --- a/pydis_site/apps/api/apps.py +++ b/pydis_site/apps/api/apps.py @@ -4,4 +4,12 @@ from django.apps import AppConfig  class ApiConfig(AppConfig):      """Django AppConfig for the API app.""" -    name = 'api' +    name = 'pydis_site.apps.api' + +    def ready(self) -> None: +        """ +        Gets called as soon as the registry is fully populated. + +        https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.ready +        """ +        import pydis_site.apps.api.signals  # noqa: F401 diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py index 8c550191..273db3d1 100644 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -60,35 +60,35 @@ domain_name_blacklist = [  ]  filter_token_blacklist = [ -    ("\bgoo+ks*\b", None, False), -    ("\bky+s+\b", None, False), -    ("\bki+ke+s*\b", None, False), -    ("\bbeaner+s?\b", None, False), -    ("\bcoo+ns*\b", None, False), -    ("\bnig+lets*\b", None, False), -    ("\bslant-eyes*\b", None, False), -    ("\btowe?l-?head+s*\b", None, False), -    ("\bchi*n+k+s*\b", None, False), -    ("\bspick*s*\b", None, False), -    ("\bkill* +(?:yo)?urself+\b", None, False), -    ("\bjew+s*\b", None, False), -    ("\bsuicide\b", None, False), -    ("\brape\b", None, False), -    ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), -    ("\bta+r+d+\b", None, False), -    ("\bcunts*\b", None, False), -    ("\btrann*y\b", None, False), -    ("\bshemale\b", None, False), -    ("fa+g+s*", None, False), -    ("卐", None, False), -    ("卍", None, False), -    ("࿖", None, False), -    ("࿕", None, False), -    ("࿘", None, False), -    ("࿗", None, False), -    ("cuck(?!oo+)", None, False), -    ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), -    ("fag+o+t+s*", None, False), +    (r"\bgoo+ks*\b", None, False), +    (r"\bky+s+\b", None, False), +    (r"\bki+ke+s*\b", None, False), +    (r"\bbeaner+s?\b", None, False), +    (r"\bcoo+ns*\b", None, False), +    (r"\bnig+lets*\b", None, False), +    (r"\bslant-eyes*\b", None, False), +    (r"\btowe?l-?head+s*\b", None, False), +    (r"\bchi*n+k+s*\b", None, False), +    (r"\bspick*s*\b", None, False), +    (r"\bkill* +(?:yo)?urself+\b", None, False), +    (r"\bjew+s*\b", None, False), +    (r"\bsuicide\b", None, False), +    (r"\brape\b", None, False), +    (r"\b(re+)tar+(d+|t+)(ed)?\b", None, False), +    (r"\bta+r+d+\b", None, False), +    (r"\bcunts*\b", None, False), +    (r"\btrann*y\b", None, False), +    (r"\bshemale\b", None, False), +    (r"fa+g+s*", None, False), +    (r"卐", None, False), +    (r"卍", None, False), +    (r"࿖", None, False), +    (r"࿕", None, False), +    (r"࿘", None, False), +    (r"࿗", None, False), +    (r"cuck(?!oo+)", None, False), +    (r"nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), +    (r"fag+o+t+s*", None, False),  ]  file_format_whitelist = [ diff --git a/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py new file mode 100644 index 00000000..dbd7ac91 --- /dev/null +++ b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.14 on 2021-05-19 05:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0069_documentationlink_validators'), +    ] + +    operations = [ +        migrations.AddField( +            model_name='offtopicchannelname', +            name='active', +            field=models.BooleanField(default=True, help_text='Whether or not this name should be considered for naming channels.'), +        ), +        migrations.AlterField( +            model_name='offtopicchannelname', +            name='used', +            field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation.'), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py new file mode 100644 index 00000000..f12efab5 --- /dev/null +++ b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-07-24 13:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0071_increase_message_content_4000'), +        ('api', '0070_auto_20210519_0545'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py b/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py new file mode 100644 index 00000000..09ad13da --- /dev/null +++ b/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.14 on 2021-09-27 20:38 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0072_doc_allow_blank_base_url'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='offtopicchannelname', +            name='name', +            field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex="^[a-z0-9\\U0001d5a0-\\U0001d5b9-ǃ?’'<>]+$")]), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py new file mode 100644 index 00000000..ebf5ae15 --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-11-05 05:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0072_merge_20210724_1354'), +        ('api', '0073_otn_allow_GT_and_LT'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/migrations/0074_reminder_failures.py b/pydis_site/apps/api/migrations/0074_reminder_failures.py new file mode 100644 index 00000000..2860046e --- /dev/null +++ b/pydis_site/apps/api/migrations/0074_reminder_failures.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-10-27 17:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0073_otn_allow_GT_and_LT'), +    ] + +    operations = [ +        migrations.AddField( +            model_name='reminder', +            name='failures', +            field=models.IntegerField(default=0, help_text='Number of times we attempted to send the reminder and failed.'), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0075_add_redirects_filter.py b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py new file mode 100644 index 00000000..23dc176f --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-11-17 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0074_reminder_failures'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='filterlist', +            name='type', +            field=models.CharField(choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token'), ('REDIRECT', 'Redirect')], help_text='The type of allowlist this is on.', max_length=50), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py new file mode 100644 index 00000000..c0ac709d --- /dev/null +++ b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-11-10 22:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0074_reminder_failures'), +    ] + +    operations = [ +        migrations.AddField( +            model_name='infraction', +            name='dm_sent', +            field=models.BooleanField(help_text='Whether a DM was sent to the user when infraction was applied.', null=True), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py new file mode 100644 index 00000000..097d0a0c --- /dev/null +++ b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.14 on 2021-11-25 19:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0075_infraction_dm_sent'), +        ('api', '0075_add_redirects_filter'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py new file mode 100644 index 00000000..9e8f2fb9 --- /dev/null +++ b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.13 on 2021-11-27 12:27 + +import django.contrib.postgres.fields +from django.db import migrations, models +import pydis_site.apps.api.models.utils + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0076_merge_20211125_1941'), +    ] + +    operations = [ +        migrations.AlterField( +            model_name='botsetting', +            name='data', +            field=models.JSONField(help_text='The actual settings of this setting.'), +        ), +        migrations.AlterField( +            model_name='deletedmessage', +            name='embeds', +            field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None), +        ), +    ] diff --git a/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py new file mode 100644 index 00000000..5ce0e871 --- /dev/null +++ b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2021-12-13 05:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + +    dependencies = [ +        ('api', '0077_use_generic_jsonfield'), +        ('api', '0074_merge_20211105_0518'), +    ] + +    operations = [ +    ] diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index 2a3944f8..1bcb1ae6 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -1,4 +1,3 @@ -from django.contrib.postgres import fields as pgfields  from django.core.exceptions import ValidationError  from django.db import models @@ -24,6 +23,6 @@ class BotSetting(ModelReprMixin, models.Model):          max_length=50,          validators=(validate_bot_setting_name,)      ) -    data = pgfields.JSONField( +    data = models.JSONField(          help_text="The actual settings of this setting."      ) diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..d30f7213 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -12,6 +12,7 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model):          'FILE_FORMAT '          'DOMAIN_NAME '          'FILTER_TOKEN ' +        'REDIRECT '      )      type = models.CharField(          max_length=50, diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index 60c1e8dd..913631d4 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -57,6 +57,10 @@ class Infraction(ModelReprMixin, models.Model):          default=False,          help_text="Whether the infraction is a shadow infraction."      ) +    dm_sent = models.BooleanField( +        null=True, +        help_text="Whether a DM was sent to the user when infraction was applied." +    )      def __str__(self):          """Returns some info on the current infraction, for display purposes.""" diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 60e2a553..bab3368d 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -48,7 +48,7 @@ class Message(ModelReprMixin, models.Model):          blank=True      )      embeds = pgfields.ArrayField( -        pgfields.JSONField( +        models.JSONField(              validators=(validate_embed,)          ),          blank=True, diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 1410250a..25741266 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,5 +1,5 @@  from django.db import models -from django_hosts.resolvers import reverse +from django.urls import reverse  from pydis_site.apps.api.models.bot.user import User  from pydis_site.apps.api.models.mixins import ModelReprMixin @@ -33,7 +33,7 @@ class MessageDeletionContext(ModelReprMixin, models.Model):      @property      def log_url(self) -> str:          """Create the url for the deleted message logs.""" -        return reverse('logs', host="staff", args=(self.id,)) +        return reverse('staff:logs', args=(self.id,))      class Meta:          """Set the ordering for list views to newest first.""" diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py index 00076248..abd25ef0 100644 --- a/pydis_site/apps/api/models/bot/metricity.py +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -4,13 +4,13 @@ from django.db import connections  BLOCK_INTERVAL = 10 * 60  # 10 minute blocks -EXCLUDE_CHANNELS = [ +EXCLUDE_CHANNELS = (      "267659945086812160",  # Bot commands      "607247579608121354"  # SeasonalBot commands -] +) -class NotFound(Exception):  # noqa: N818 +class NotFoundError(Exception):  # noqa: N818      """Raised when an entity cannot be found."""      pass @@ -37,7 +37,7 @@ class Metricity:          values = self.cursor.fetchone()          if not values: -            raise NotFound() +            raise NotFoundError()          return dict(zip(columns, values)) @@ -46,19 +46,19 @@ class Metricity:          self.cursor.execute(              """              SELECT -              COUNT(*) +                COUNT(*)              FROM messages              WHERE -              author_id = '%s' -              AND NOT is_deleted -              AND NOT %s::varchar[] @> ARRAY[channel_id] +                author_id = '%s' +                AND NOT is_deleted +                AND channel_id NOT IN %s              """,              [user_id, EXCLUDE_CHANNELS]          )          values = self.cursor.fetchone()          if not values: -            raise NotFound() +            raise NotFoundError()          return values[0] @@ -79,7 +79,7 @@ class Metricity:                  WHERE                      author_id='%s'                      AND NOT is_deleted -                    AND NOT %s::varchar[] @> ARRAY[channel_id] +                    AND channel_id NOT IN %s                  GROUP BY interval              ) block_query;              """, @@ -88,7 +88,7 @@ class Metricity:          values = self.cursor.fetchone()          if not values: -            raise NotFound() +            raise NotFoundError()          return values[0] @@ -127,6 +127,6 @@ class Metricity:          values = self.cursor.fetchall()          if not values: -            raise NotFound() +            raise NotFoundError()          return values diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 403c7465..e9fec114 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -11,14 +11,19 @@ class OffTopicChannelName(ModelReprMixin, models.Model):          primary_key=True,          max_length=96,          validators=( -            RegexValidator(regex=r"^[a-z0-9\U0001d5a0-\U0001d5b9-ǃ?’']+$"), +            RegexValidator(regex=r"^[a-z0-9\U0001d5a0-\U0001d5b9-ǃ?’'<>]+$"),          ),          help_text="The actual channel name that will be used on our Discord server."      )      used = models.BooleanField(          default=False, -        help_text="Whether or not this name has already been used during this rotation", +        help_text="Whether or not this name has already been used during this rotation.", +    ) + +    active = models.BooleanField( +        default=True, +        help_text="Whether or not this name should be considered for naming channels."      )      def __str__(self): diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index 7d968a0e..173900ee 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -59,6 +59,10 @@ class Reminder(ModelReprMixin, models.Model):          blank=True,          help_text="IDs of roles or users to ping with the reminder."      ) +    failures = models.IntegerField( +        default=0, +        help_text="Number of times we attempted to send the reminder and failed." +    )      def __str__(self):          """Returns some info on the current reminder, for display purposes.""" diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 0e220a1d..859394d2 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -103,11 +103,10 @@ def validate_embed(embed: Any) -> None:      Example: -        >>> from django.contrib.postgres import fields as pgfields          >>> from django.db import models          >>> from pydis_site.apps.api.models.utils import validate_embed          >>> class MyMessage(models.Model): -        ...     embed = pgfields.JSONField( +        ...     embed = models.JSONField(          ...         validators=(          ...             validate_embed,          ...         ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f47bedca..ac05ebd4 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -145,7 +145,16 @@ class InfractionSerializer(ModelSerializer):          model = Infraction          fields = ( -            'id', 'inserted_at', 'expires_at', 'active', 'user', 'actor', 'type', 'reason', 'hidden' +            'id', +            'inserted_at', +            'expires_at', +            'active', +            'user', +            'actor', +            'type', +            'reason', +            'hidden', +            'dm_sent'          )          validators = [              UniqueTogetherValidator( @@ -200,25 +209,30 @@ class ExpandedInfractionSerializer(InfractionSerializer):          return ret +class OffTopicChannelNameListSerializer(ListSerializer): +    """Custom ListSerializer to override to_representation() when list views are triggered.""" + +    def to_representation(self, objects: list[OffTopicChannelName]) -> list[str]: +        """ +        Return a list with all `OffTopicChannelName`s in the database. + +        This returns the list of off topic channel names. We want to only return +        the name attribute, hence it is unnecessary to create a nested dictionary. +        Additionally, this allows off topic channel name routes to simply return an +        array of names instead of objects, saving on bandwidth. +        """ +        return [obj.name for obj in objects] + +  class OffTopicChannelNameSerializer(ModelSerializer):      """A class providing (de-)serialization of `OffTopicChannelName` instances."""      class Meta:          """Metadata defined for the Django REST Framework.""" +        list_serializer_class = OffTopicChannelNameListSerializer          model = OffTopicChannelName -        fields = ('name',) - -    def to_representation(self, obj: OffTopicChannelName) -> str: -        """ -        Return the representation of this `OffTopicChannelName`. - -        This only returns the name of the off topic channel name. As the model -        only has a single attribute, it is unnecessary to create a nested dictionary. -        Additionally, this allows off topic channel name routes to simply return an -        array of names instead of objects, saving on bandwidth. -        """ -        return obj.name +        fields = ('name', 'used', 'active')  class ReminderSerializer(ModelSerializer): @@ -231,7 +245,15 @@ class ReminderSerializer(ModelSerializer):          model = Reminder          fields = ( -            'active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id', 'mentions' +            'active', +            'author', +            'jump_url', +            'channel_id', +            'content', +            'expiration', +            'id', +            'mentions', +            'failures'          ) diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py new file mode 100644 index 00000000..5c26bfb6 --- /dev/null +++ b/pydis_site/apps/api/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_delete +from django.dispatch import receiver + +from pydis_site.apps.api.models.bot import Role, User + + +@receiver(signal=post_delete, sender=Role) +def delete_role_from_user(sender: Role, instance: Role, **kwargs) -> None: +    """Unassigns the Role (instance) that is being deleted from every user that has it.""" +    for user in User.objects.filter(roles__contains=[instance.id]): +        del user.roles[user.roles.index(instance.id)] +        user.save() diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py index 61c23b0f..c9f3cb7e 100644 --- a/pydis_site/apps/api/tests/base.py +++ b/pydis_site/apps/api/tests/base.py @@ -11,7 +11,7 @@ test_user, _created = User.objects.get_or_create(  ) -class APISubdomainTestCase(APITestCase): +class AuthenticatedAPITestCase(APITestCase):      """      Configures the test client. @@ -24,14 +24,13 @@ class APISubdomainTestCase(APITestCase):      `self.client.force_authenticate(user=created_user)` to force authentication      through the created user. -    Using this performs the following niceties for you which ease writing tests: -    - setting the `HTTP_HOST` request header to `api.pythondiscord.local:8000`, and +    Using this performs the following nicety for you which eases writing tests:      - forcing authentication for the test user.      If you don't want to force authentication (for example, to test a route's response      for an unauthenticated user), un-force authentication by using the following: -    >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase -    >>> class UnauthedUserTestCase(APISubdomainTestCase): +    >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase +    >>> class UnauthedUserTestCase(AuthenticatedAPITestCase):      ...     def setUp(self):      ...         super().setUp()      ...         self.client.force_authentication(user=None) @@ -42,30 +41,26 @@ class APISubdomainTestCase(APITestCase):      ...         resp = self.client.delete('/my-publicly-readable-endpoint/42')      ...         self.assertEqual(resp.status_code, 401) -    Make sure to include the `super().setUp(self)` call, otherwise, you may get -    status code 404 for some URLs due to the missing `HTTP_HOST` header. -      ## Example      Using this in a test case is rather straightforward: -    >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase -    >>> class MyAPITestCase(APISubdomainTestCase): +    >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase +    >>> class MyAPITestCase(AuthenticatedAPITestCase):      ...     def test_that_it_works(self):      ...         response = self.client.get('/my-endpoint')      ...         self.assertEqual(response.status_code, 200) -    To reverse URLs of the API host, you need to use `django_hosts`: +    To reverse URLs of the API host, you need to use `django.urls`: -    >>> from django_hosts.resolvers import reverse -    >>> from pydis_site.apps.api.tests.base import APISubdomainTestCase -    >>> class MyReversedTestCase(APISubdomainTestCase): +    >>> from django.urls import reverse +    >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase +    >>> class MyReversedTestCase(AuthenticatedAPITestCase):      ...     def test_my_endpoint(self): -    ...         url = reverse('user-detail', host='api') +    ...         url = reverse('api:user-detail')      ...         response = self.client.get(url)      ...         self.assertEqual(response.status_code, 200)      """      def setUp(self):          super().setUp() -        self.client.defaults['HTTP_HOST'] = 'api.pythondiscord.local:8000'          self.client.force_authenticate(test_user) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index 40450844..1eb535d8 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -1,13 +1,13 @@  from datetime import datetime +from django.urls import reverse  from django.utils import timezone -from django_hosts.resolvers import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import MessageDeletionContext, User -class DeletedMessagesWithoutActorTests(APISubdomainTestCase): +class DeletedMessagesWithoutActorTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -40,14 +40,14 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase):          }      def test_accepts_valid_data(self): -        url = reverse('bot:messagedeletioncontext-list', host='api') +        url = reverse('api:bot:messagedeletioncontext-list')          response = self.client.post(url, data=self.data)          self.assertEqual(response.status_code, 201)          [context] = MessageDeletionContext.objects.all()          self.assertIsNone(context.actor) -class DeletedMessagesWithActorTests(APISubdomainTestCase): +class DeletedMessagesWithActorTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = cls.actor = User.objects.create( @@ -72,14 +72,14 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase):          }      def test_accepts_valid_data_and_sets_actor(self): -        url = reverse('bot:messagedeletioncontext-list', host='api') +        url = reverse('api:bot:messagedeletioncontext-list')          response = self.client.post(url, data=self.data)          self.assertEqual(response.status_code, 201)          [context] = MessageDeletionContext.objects.all()          self.assertEqual(context.actor.id, self.actor.id) -class DeletedMessagesLogURLTests(APISubdomainTestCase): +class DeletedMessagesLogURLTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = cls.actor = User.objects.create( @@ -94,6 +94,6 @@ class DeletedMessagesLogURLTests(APISubdomainTestCase):          )      def test_valid_log_url(self): -        expected_url = reverse('logs', host="staff", args=(1,)) +        expected_url = reverse('staff:logs', args=(1,))          [context] = MessageDeletionContext.objects.all()          self.assertEqual(context.log_url, expected_url) diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py index 39fb08f3..4e238cbb 100644 --- a/pydis_site/apps/api/tests/test_documentation_links.py +++ b/pydis_site/apps/api/tests/test_documentation_links.py @@ -1,61 +1,61 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import DocumentationLink -class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): +class UnauthedDocumentationLinkAPITests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_detail_lookup_returns_401(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_list_returns_401(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_create_returns_401(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.post(url, data={'hi': 'there'})          self.assertEqual(response.status_code, 401)      def test_delete_returns_401(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 401) -class EmptyDatabaseDocumentationLinkAPITests(APISubdomainTestCase): +class EmptyDatabaseDocumentationLinkAPITests(AuthenticatedAPITestCase):      def test_detail_lookup_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.get(url)          self.assertEqual(response.status_code, 404)      def test_list_all_returns_empty_list(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), [])      def test_delete_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404) -class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase): +class DetailLookupDocumentationLinkAPITests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.doc_link = DocumentationLink.objects.create( @@ -71,27 +71,27 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):          }      def test_detail_lookup_unknown_package_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.get(url)          self.assertEqual(response.status_code, 404)      def test_detail_lookup_created_package_returns_package(self): -        url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') +        url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,))          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), self.doc_json)      def test_list_all_packages_shows_created_package(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), [self.doc_json])      def test_create_invalid_body_returns_400(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.post(url, data={'i': 'am', 'totally': 'valid'})          self.assertEqual(response.status_code, 400) @@ -103,7 +103,7 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):              'inventory_url': 'totally an url'          } -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.post(url, data=body)          self.assertEqual(response.status_code, 400) @@ -114,13 +114,13 @@ class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):              with self.subTest(package_name=case):                  body = self.doc_json.copy()                  body['package'] = case -                url = reverse('bot:documentationlink-list', host='api') +                url = reverse('api:bot:documentationlink-list')                  response = self.client.post(url, data=body)                  self.assertEqual(response.status_code, 400) -class DocumentationLinkCreationTests(APISubdomainTestCase): +class DocumentationLinkCreationTests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp() @@ -130,27 +130,27 @@ class DocumentationLinkCreationTests(APISubdomainTestCase):              'inventory_url': 'https://docs.example.com'          } -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.post(url, data=self.body)          self.assertEqual(response.status_code, 201)      def test_package_in_full_list(self): -        url = reverse('bot:documentationlink-list', host='api') +        url = reverse('api:bot:documentationlink-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), [self.body])      def test_detail_lookup_works_with_package(self): -        url = reverse('bot:documentationlink-detail', args=(self.body['package'],), host='api') +        url = reverse('api:bot:documentationlink-detail', args=(self.body['package'],))          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), self.body) -class DocumentationLinkDeletionTests(APISubdomainTestCase): +class DocumentationLinkDeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.doc_link = DocumentationLink.objects.create( @@ -160,13 +160,13 @@ class DocumentationLinkDeletionTests(APISubdomainTestCase):          )      def test_unknown_package_returns_404(self): -        url = reverse('bot:documentationlink-detail', args=('whatever',), host='api') +        url = reverse('api:bot:documentationlink-detail', args=('whatever',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404)      def test_delete_known_package_returns_204(self): -        url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api') +        url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204) diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py index 188c0fff..5a5bca60 100644 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -1,9 +1,9 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse  from pydis_site.apps.api.models import FilterList -from pydis_site.apps.api.tests.base import APISubdomainTestCase +from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase -URL = reverse('bot:filterlist-list', host='api') +URL = reverse('api:bot:filterlist-list')  JPEG_ALLOWLIST = {      "type": 'FILE_FORMAT',      "allowed": True, @@ -16,7 +16,7 @@ PNG_ALLOWLIST = {  } -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None) @@ -27,7 +27,7 @@ class UnauthenticatedTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 401) -class EmptyDatabaseTests(APISubdomainTestCase): +class EmptyDatabaseTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          FilterList.objects.all().delete() @@ -39,7 +39,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):          self.assertEqual(response.json(), []) -class FetchTests(APISubdomainTestCase): +class FetchTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          FilterList.objects.all().delete() @@ -68,7 +68,7 @@ class FetchTests(APISubdomainTestCase):              self.assertEquals(api_type[1], model_type[1]) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          FilterList.objects.all().delete() @@ -103,7 +103,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          FilterList.objects.all().delete() diff --git a/pydis_site/apps/api/tests/test_healthcheck.py b/pydis_site/apps/api/tests/test_healthcheck.py index b0fd71bf..650403ad 100644 --- a/pydis_site/apps/api/tests/test_healthcheck.py +++ b/pydis_site/apps/api/tests/test_healthcheck.py @@ -1,15 +1,15 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase -class UnauthedHealthcheckAPITests(APISubdomainTestCase): +class UnauthedHealthcheckAPITests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_can_access_healthcheck_view(self): -        url = reverse('healthcheck', host='api') +        url = reverse('api:healthcheck')          response = self.client.get(url)          self.assertEqual(response.status_code, 200) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index 9aae16c0..b3dd16ee 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -4,44 +4,44 @@ from unittest.mock import patch  from urllib.parse import quote  from django.db.utils import IntegrityError -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import Infraction, User  from ..serializers import InfractionSerializer -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_detail_lookup_returns_401(self): -        url = reverse('bot:infraction-detail', args=(6,), host='api') +        url = reverse('api:bot:infraction-detail', args=(6,))          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_list_returns_401(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_create_returns_401(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.post(url, data={'reason': 'Have a nice day.'})          self.assertEqual(response.status_code, 401)      def test_partial_update_returns_401(self): -        url = reverse('bot:infraction-detail', args=(6,), host='api') +        url = reverse('api:bot:infraction-detail', args=(6,))          response = self.client.patch(url, data={'reason': 'Have a nice day.'})          self.assertEqual(response.status_code, 401) -class InfractionTests(APISubdomainTestCase): +class InfractionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -92,7 +92,7 @@ class InfractionTests(APISubdomainTestCase):      def test_list_all(self):          """Tests the list-view, which should be ordered by inserted_at (newest first).""" -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200) @@ -106,7 +106,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[4]['id'], self.ban_hidden.id)      def test_filter_search(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          pattern = quote(r'^James(\s\w+){3},')          response = self.client.get(f'{url}?search={pattern}') @@ -117,7 +117,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[0]['id'], self.ban_inactive.id)      def test_filter_field(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?type=ban&hidden=true')          self.assertEqual(response.status_code, 200) @@ -127,7 +127,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[0]['id'], self.ban_hidden.id)      def test_filter_permanent_false(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?type=mute&permanent=false')          self.assertEqual(response.status_code, 200) @@ -136,7 +136,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(len(infractions), 0)      def test_filter_permanent_true(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?type=mute&permanent=true')          self.assertEqual(response.status_code, 200) @@ -145,7 +145,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[0]['id'], self.mute_permanent.id)      def test_filter_after(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)          response = self.client.get(f'{url}?type=superstar&expires_after={target_time.isoformat()}') @@ -154,7 +154,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(len(infractions), 0)      def test_filter_before(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)          response = self.client.get(f'{url}?type=superstar&expires_before={target_time.isoformat()}') @@ -164,21 +164,21 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id)      def test_filter_after_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?expires_after=gibberish')          self.assertEqual(response.status_code, 400)          self.assertEqual(list(response.json())[0], "expires_after")      def test_filter_before_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?expires_before=000000000')          self.assertEqual(response.status_code, 400)          self.assertEqual(list(response.json())[0], "expires_before")      def test_after_before_before(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=4)          target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=6)          response = self.client.get( @@ -191,7 +191,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id)      def test_after_after_before_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)          target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=9)          response = self.client.get( @@ -205,7 +205,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertIn("expires_after", errors)      def test_permanent_after_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)          response = self.client.get(f'{url}?permanent=true&expires_after={target_time.isoformat()}') @@ -214,7 +214,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual("permanent", errors[0])      def test_permanent_before_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)          response = self.client.get(f'{url}?permanent=true&expires_before={target_time.isoformat()}') @@ -223,7 +223,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual("permanent", errors[0])      def test_nonpermanent_before(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=6)          response = self.client.get(              f'{url}?permanent=false&expires_before={target_time.isoformat()}' @@ -234,7 +234,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id)      def test_filter_manytypes(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?types=mute,ban')          self.assertEqual(response.status_code, 200) @@ -242,7 +242,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(len(infractions), 3)      def test_types_type_invalid(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?types=mute,ban&type=superstar')          self.assertEqual(response.status_code, 400) @@ -250,7 +250,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual("types", errors[0])      def test_sort_expiresby(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?ordering=expires_at&permanent=false')          self.assertEqual(response.status_code, 200)          infractions = response.json() @@ -261,34 +261,34 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infractions[2]['id'], self.ban_hidden.id)      def test_returns_empty_for_no_match(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?type=ban&search=poop')          self.assertEqual(response.status_code, 200)          self.assertEqual(len(response.json()), 0)      def test_ignores_bad_filters(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          response = self.client.get(f'{url}?type=ban&hidden=maybe&foo=bar')          self.assertEqual(response.status_code, 200)          self.assertEqual(len(response.json()), 2)      def test_retrieve_single_from_id(self): -        url = reverse('bot:infraction-detail', args=(self.ban_inactive.id,), host='api') +        url = reverse('api:bot:infraction-detail', args=(self.ban_inactive.id,))          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json()['id'], self.ban_inactive.id)      def test_retrieve_returns_404_for_absent_id(self): -        url = reverse('bot:infraction-detail', args=(1337,), host='api') +        url = reverse('api:bot:infraction-detail', args=(1337,))          response = self.client.get(url)          self.assertEqual(response.status_code, 404)      def test_partial_update(self): -        url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') +        url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,))          data = {              'expires_at': '4143-02-15T21:04:31+00:00',              'active': False, @@ -313,7 +313,7 @@ class InfractionTests(APISubdomainTestCase):          self.assertEqual(infraction.hidden, self.ban_hidden.hidden)      def test_partial_update_returns_400_for_frozen_field(self): -        url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api') +        url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,))          data = {'user': 6}          response = self.client.patch(url, data=data) @@ -323,7 +323,7 @@ class InfractionTests(APISubdomainTestCase):          }) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -338,7 +338,7 @@ class CreationTests(APISubdomainTestCase):          )      def test_accepts_valid_data(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'user': self.user.id,              'actor': self.user.id, @@ -367,7 +367,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(infraction.active, True)      def test_returns_400_for_missing_user(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'actor': self.user.id,              'type': 'kick', @@ -381,7 +381,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_bad_user(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'user': 1337,              'actor': self.user.id, @@ -396,7 +396,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_bad_type(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'user': self.user.id,              'actor': self.user.id, @@ -411,7 +411,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_bad_expired_at_format(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'user': self.user.id,              'actor': self.user.id, @@ -430,7 +430,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_expiring_non_expirable_type(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          for infraction_type in ('kick', 'warning'):              data = { @@ -448,7 +448,7 @@ class CreationTests(APISubdomainTestCase):              })      def test_returns_400_for_hidden_non_hideable_type(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          for infraction_type in ('superstar', 'warning'):              data = { @@ -466,7 +466,7 @@ class CreationTests(APISubdomainTestCase):              })      def test_returns_400_for_non_hidden_required_hidden_type(self): -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          data = {              'user': self.user.id, @@ -484,7 +484,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_active_infraction_of_type_that_cannot_be_active(self):          """Test if the API rejects active infractions for types that cannot be active.""" -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          restricted_types = (              ('note', True),              ('warning', False), @@ -511,7 +511,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_second_active_infraction_of_the_same_type(self):          """Test if the API rejects a second active infraction of the same type for a given user.""" -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          active_infraction_types = ('mute', 'ban', 'superstar')          for infraction_type in active_infraction_types: @@ -550,7 +550,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_201_for_second_active_infraction_of_different_type(self):          """Test if the API accepts a second active infraction of a different type than the first.""" -        url = reverse('bot:infraction-list', host='api') +        url = reverse('api:bot:infraction-list')          first_active_infraction = {              'user': self.user.id,              'actor': self.user.id, @@ -677,7 +677,7 @@ class CreationTests(APISubdomainTestCase):              ) -class InfractionDeletionTests(APISubdomainTestCase): +class InfractionDeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -694,20 +694,20 @@ class InfractionDeletionTests(APISubdomainTestCase):          )      def test_delete_unknown_infraction_returns_404(self): -        url = reverse('bot:infraction-detail', args=('something',), host='api') +        url = reverse('api:bot:infraction-detail', args=('something',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404)      def test_delete_known_infraction_returns_204(self): -        url = reverse('bot:infraction-detail', args=(self.warning.id,), host='api') +        url = reverse('api:bot:infraction-detail', args=(self.warning.id,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204)          self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id) -class ExpandedTests(APISubdomainTestCase): +class ExpandedTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -735,7 +735,7 @@ class ExpandedTests(APISubdomainTestCase):                  self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}')      def test_list_expanded(self): -        url = reverse('bot:infraction-list-expanded', host='api') +        url = reverse('api:bot:infraction-list-expanded')          response = self.client.get(url)          self.assertEqual(response.status_code, 200) @@ -747,7 +747,7 @@ class ExpandedTests(APISubdomainTestCase):              self.check_expanded_fields(infraction)      def test_create_expanded(self): -        url = reverse('bot:infraction-list-expanded', host='api') +        url = reverse('api:bot:infraction-list-expanded')          data = {              'user': self.user.id,              'actor': self.user.id, @@ -762,7 +762,7 @@ class ExpandedTests(APISubdomainTestCase):          self.check_expanded_fields(response.json())      def test_retrieve_expanded(self): -        url = reverse('bot:infraction-detail-expanded', args=(self.warning.id,), host='api') +        url = reverse('api:bot:infraction-detail-expanded', args=(self.warning.id,))          response = self.client.get(url)          self.assertEqual(response.status_code, 200) @@ -772,7 +772,7 @@ class ExpandedTests(APISubdomainTestCase):          self.check_expanded_fields(infraction)      def test_partial_update_expanded(self): -        url = reverse('bot:infraction-detail-expanded', args=(self.kick.id,), host='api') +        url = reverse('api:bot:infraction-detail-expanded', args=(self.kick.id,))          data = {'active': False}          response = self.client.patch(url, data=data) @@ -783,7 +783,7 @@ class ExpandedTests(APISubdomainTestCase):          self.check_expanded_fields(response.json()) -class SerializerTests(APISubdomainTestCase): +class SerializerTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 9cefbd8f..62b2314c 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -1,12 +1,12 @@  from datetime import datetime as dt, timedelta, timezone -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import Nomination, NominationEntry, User -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -21,7 +21,7 @@ class CreationTests(APISubdomainTestCase):          )      def test_accepts_valid_data(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'actor': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -46,7 +46,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(nomination.active, True)      def test_returns_200_on_second_active_nomination_by_different_user(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          first_data = {              'actor': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -65,7 +65,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response2.status_code, 201)      def test_returns_400_on_second_active_nomination_by_existing_nominator(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'actor': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -82,7 +82,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_missing_user(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'actor': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -95,7 +95,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_missing_actor(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -108,7 +108,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_201_for_missing_reason(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'actor': self.user.id, @@ -118,7 +118,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 201)      def test_returns_400_for_bad_user(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': 1024,              'reason': 'Joe Dart on Fender Bass', @@ -132,7 +132,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_bad_actor(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -146,7 +146,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_end_reason_at_creation(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -161,7 +161,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_ended_at_at_creation(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -176,7 +176,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_inserted_at_at_creation(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -191,7 +191,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_for_active_at_creation(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          data = {              'user': self.user.id,              'reason': 'Joe Dart on Fender Bass', @@ -206,7 +206,7 @@ class CreationTests(APISubdomainTestCase):          }) -class NominationTests(APISubdomainTestCase): +class NominationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.user = User.objects.create( @@ -236,7 +236,7 @@ class NominationTests(APISubdomainTestCase):          )      def test_returns_200_update_reason_on_active_with_actor(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {              'reason': "He's one funky duck",              'actor': self.user.id @@ -252,7 +252,7 @@ class NominationTests(APISubdomainTestCase):          self.assertEqual(nomination_entry.reason, data['reason'])      def test_returns_400_on_frozen_field_update(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {              'user': "Theo Katzman"          } @@ -264,7 +264,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_400_update_end_reason_on_active(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {              'end_reason': 'He started playing jazz'          } @@ -276,7 +276,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_200_update_reason_on_inactive(self): -        url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,))          data = {              'reason': "He's one funky duck",              'actor': self.user.id @@ -292,7 +292,7 @@ class NominationTests(APISubdomainTestCase):          self.assertEqual(nomination_entry.reason, data['reason'])      def test_returns_200_update_end_reason_on_inactive(self): -        url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,))          data = {              'end_reason': 'He started playing jazz'          } @@ -305,9 +305,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_200_on_valid_end_nomination(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(self.active_nomination.id,), -            host='api'          )          data = {              'active': False, @@ -328,9 +327,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_400_on_invalid_field_end_nomination(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(self.active_nomination.id,), -            host='api'          )          data = {              'active': False, @@ -344,9 +342,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_400_on_missing_end_reason_end_nomination(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(self.active_nomination.id,), -            host='api'          )          data = {              'active': False, @@ -360,9 +357,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_400_on_invalid_use_of_active(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(self.inactive_nomination.id,), -            host='api'          )          data = {              'active': False, @@ -376,9 +372,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_404_on_get_unknown_nomination(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(9999,), -            host='api'          )          response = self.client.get(url, data={}) @@ -389,9 +384,8 @@ class NominationTests(APISubdomainTestCase):      def test_returns_404_on_patch_unknown_nomination(self):          url = reverse( -            'bot:nomination-detail', +            'api:bot:nomination-detail',              args=(9999,), -            host='api'          )          response = self.client.patch(url, data={}) @@ -401,7 +395,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_list_put(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          response = self.client.put(url, data={})          self.assertEqual(response.status_code, 405) @@ -410,7 +404,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_list_patch(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          response = self.client.patch(url, data={})          self.assertEqual(response.status_code, 405) @@ -419,7 +413,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_list_delete(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          response = self.client.delete(url, data={})          self.assertEqual(response.status_code, 405) @@ -428,7 +422,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_detail_post(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          response = self.client.post(url, data={})          self.assertEqual(response.status_code, 405) @@ -437,7 +431,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_detail_delete(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          response = self.client.delete(url, data={})          self.assertEqual(response.status_code, 405) @@ -446,7 +440,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_returns_405_on_detail_put(self): -        url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          response = self.client.put(url, data={})          self.assertEqual(response.status_code, 405) @@ -455,7 +449,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_filter_returns_0_objects_unknown_user__id(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          response = self.client.get(              url, @@ -470,7 +464,7 @@ class NominationTests(APISubdomainTestCase):          self.assertEqual(len(infractions), 0)      def test_filter_returns_2_objects_for_testdata(self): -        url = reverse('bot:nomination-list', host='api') +        url = reverse('api:bot:nomination-list')          response = self.client.get(              url, @@ -485,14 +479,14 @@ class NominationTests(APISubdomainTestCase):          self.assertEqual(len(infractions), 2)      def test_patch_nomination_set_reviewed_of_active_nomination(self): -        url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {'reviewed': True}          response = self.client.patch(url, data=data)          self.assertEqual(response.status_code, 200)      def test_patch_nomination_set_reviewed_of_inactive_nomination(self): -        url = reverse('api:nomination-detail', args=(self.inactive_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,))          data = {'reviewed': True}          response = self.client.patch(url, data=data) @@ -502,7 +496,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_patch_nomination_set_reviewed_and_end(self): -        url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {'reviewed': True, 'active': False, 'end_reason': "What?"}          response = self.client.patch(url, data=data) @@ -512,7 +506,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_modifying_reason_without_actor(self): -        url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {'reason': 'That is my reason!'}          response = self.client.patch(url, data=data) @@ -522,7 +516,7 @@ class NominationTests(APISubdomainTestCase):          })      def test_modifying_reason_with_unknown_actor(self): -        url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api') +        url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))          data = {'reason': 'That is my reason!', 'actor': 90909090909090}          response = self.client.patch(url, data=data) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 3ab8b22d..2d273756 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -1,33 +1,33 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import OffTopicChannelName -class UnauthenticatedTests(APISubdomainTestCase): +class UnauthenticatedTests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_cannot_read_off_topic_channel_name_list(self):          """Return a 401 response when not authenticated.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self):          """Return a 401 response when `random_items` provided and not authenticated.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(f'{url}?random_items=no')          self.assertEqual(response.status_code, 401) -class EmptyDatabaseTests(APISubdomainTestCase): +class EmptyDatabaseTests(AuthenticatedAPITestCase):      def test_returns_empty_object(self):          """Return empty list when no names in database.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200) @@ -35,7 +35,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):      def test_returns_empty_list_with_get_all_param(self):          """Return empty list when no names and `random_items` param provided.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(f'{url}?random_items=5')          self.assertEqual(response.status_code, 200) @@ -43,7 +43,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):      def test_returns_400_for_bad_random_items_param(self):          """Return error message when passing not integer as `random_items`.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(f'{url}?random_items=totally-a-valid-integer')          self.assertEqual(response.status_code, 400) @@ -53,7 +53,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):      def test_returns_400_for_negative_random_items_param(self):          """Return error message when passing negative int as `random_items`.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(f'{url}?random_items=-5')          self.assertEqual(response.status_code, 400) @@ -62,56 +62,89 @@ class EmptyDatabaseTests(APISubdomainTestCase):          }) -class ListTests(APISubdomainTestCase): +class ListTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls): -        cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) -        cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) +        cls.test_name = OffTopicChannelName.objects.create( +            name='lemons-lemonade-stand', used=False, active=True +        ) +        cls.test_name_2 = OffTopicChannelName.objects.create( +            name='bbq-with-bisk', used=False, active=True +        ) +        cls.test_name_3 = OffTopicChannelName.objects.create( +            name="frozen-with-iceman", used=True, active=False +        )      def test_returns_name_in_list(self):          """Return all off-topic channel names.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual( -            response.json(), -            [ +            set(response.json()), +            {                  self.test_name.name, -                self.test_name_2.name -            ] +                self.test_name_2.name, +                self.test_name_3.name +            }          ) -    def test_returns_single_item_with_random_items_param_set_to_1(self): +    def test_returns_two_items_with_random_items_param_set_to_2(self):          """Return not-used name instead used.""" -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=1') +        url = reverse('api:bot:offtopicchannelname-list') +        response = self.client.get(f'{url}?random_items=2')          self.assertEqual(response.status_code, 200) -        self.assertEqual(len(response.json()), 1) -        self.assertEqual(response.json(), [self.test_name.name]) +        self.assertEqual(len(response.json()), 2) +        self.assertEqual(set(response.json()), {self.test_name.name, self.test_name_2.name})      def test_running_out_of_names_with_random_parameter(self):          """Reset names `used` parameter to `False` when running out of names.""" -        url = reverse('bot:offtopicchannelname-list', host='api') -        response = self.client.get(f'{url}?random_items=2') +        url = reverse('api:bot:offtopicchannelname-list') +        response = self.client.get(f'{url}?random_items=3')          self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) +        self.assertEqual( +            set(response.json()), +            {self.test_name.name, self.test_name_2.name, self.test_name_3.name} +        ) + +    def test_returns_inactive_ot_names(self): +        """Return inactive off topic names.""" +        url = reverse('api:bot:offtopicchannelname-list') +        response = self.client.get(f"{url}?active=false") + +        self.assertEqual(response.status_code, 200) +        self.assertEqual( +            response.json(), +            [self.test_name_3.name] +        ) + +    def test_returns_active_ot_names(self): +        """Return active off topic names.""" +        url = reverse('api:bot:offtopicchannelname-list') +        response = self.client.get(f"{url}?active=true") + +        self.assertEqual(response.status_code, 200) +        self.assertEqual( +            set(response.json()), +            {self.test_name.name, self.test_name_2.name} +        ) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp() -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          self.name = "abcdefghijklmnopqrstuvwxyz-0123456789"          response = self.client.post(f'{url}?name={self.name}')          self.assertEqual(response.status_code, 201)      def test_returns_201_for_unicode_chars(self):          """Accept all valid characters.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          names = (              '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹',              'ǃ?’', @@ -123,7 +156,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_missing_name_param(self):          """Return error message when name not provided.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.post(url)          self.assertEqual(response.status_code, 400)          self.assertEqual(response.json(), { @@ -132,7 +165,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_bad_name_param(self):          """Return error message when invalid characters provided.""" -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          invalid_names = (              'space between words',              'ABCDEFGHIJKLMNOPQRSTUVWXYZ', @@ -147,33 +180,33 @@ class CreationTests(APISubdomainTestCase):              }) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')          cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')      def test_deleting_unknown_name_returns_404(self): -        """Return 404 reponse when trying to delete unknown name.""" -        url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') +        """Return 404 response when trying to delete unknown name.""" +        url = reverse('api:bot:offtopicchannelname-detail', args=('unknown-name',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404)      def test_deleting_known_name_returns_204(self):          """Return 204 response when deleting was successful.""" -        url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') +        url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name.name,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204)      def test_name_gets_deleted(self):          """Name gets actually deleted.""" -        url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') +        url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name_2.name,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204) -        url = reverse('bot:offtopicchannelname-list', host='api') +        url = reverse('api:bot:offtopicchannelname-list')          response = self.client.get(url)          self.assertNotIn(self.test_name_2.name, response.json()) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py index 0f3dbffa..3cf95b75 100644 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ b/pydis_site/apps/api/tests/test_offensive_message.py @@ -1,14 +1,14 @@  import datetime -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import OffensiveMessage -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      def test_accept_valid_data(self): -        url = reverse('bot:offensivemessage-list', host='api') +        url = reverse('api:bot:offensivemessage-list')          delete_at = datetime.datetime.now() + datetime.timedelta(days=1)          data = {              'id': '602951077675139072', @@ -31,7 +31,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(data['channel_id'], str(offensive_message.channel_id))      def test_returns_400_on_non_future_date(self): -        url = reverse('bot:offensivemessage-list', host='api') +        url = reverse('api:bot:offensivemessage-list')          delete_at = datetime.datetime.now() - datetime.timedelta(days=1)          data = {              'id': '602951077675139072', @@ -45,7 +45,7 @@ class CreationTests(APISubdomainTestCase):          })      def test_returns_400_on_negative_id_or_channel_id(self): -        url = reverse('bot:offensivemessage-list', host='api') +        url = reverse('api:bot:offensivemessage-list')          delete_at = datetime.datetime.now() + datetime.timedelta(days=1)          data = {              'id': '602951077675139072', @@ -58,7 +58,7 @@ class CreationTests(APISubdomainTestCase):          )          for field, invalid_value in cases: -            with self.subTest(fied=field, invalid_value=invalid_value): +            with self.subTest(field=field, invalid_value=invalid_value):                  test_data = data.copy()                  test_data.update({field: invalid_value}) @@ -69,7 +69,7 @@ class CreationTests(APISubdomainTestCase):                  }) -class ListTests(APISubdomainTestCase): +class ListTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          delete_at = datetime.datetime.now() + datetime.timedelta(days=1) @@ -100,7 +100,7 @@ class ListTests(APISubdomainTestCase):          cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z'      def test_get_data(self): -        url = reverse('bot:offensivemessage-list', host='api') +        url = reverse('api:bot:offensivemessage-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200) @@ -108,7 +108,7 @@ class ListTests(APISubdomainTestCase):          self.assertEqual(response.json(), self.messages) -class DeletionTests(APISubdomainTestCase): +class DeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) @@ -121,7 +121,7 @@ class DeletionTests(APISubdomainTestCase):      def test_delete_data(self):          url = reverse( -            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +            'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,)          )          response = self.client.delete(url) @@ -132,7 +132,7 @@ class DeletionTests(APISubdomainTestCase):          ) -class NotAllowedMethodsTests(APISubdomainTestCase): +class NotAllowedMethodsTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) @@ -145,7 +145,7 @@ class NotAllowedMethodsTests(APISubdomainTestCase):      def test_returns_405_for_patch_and_put_requests(self):          url = reverse( -            'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,) +            'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,)          )          not_allowed_methods = (self.client.patch, self.client.put) diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index 9dffb668..709685bc 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -1,52 +1,52 @@  from datetime import datetime  from django.forms.models import model_to_dict -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import Reminder, User -class UnauthedReminderAPITests(APISubdomainTestCase): +class UnauthedReminderAPITests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_list_returns_401(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_create_returns_401(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.post(url, data={'not': 'important'})          self.assertEqual(response.status_code, 401)      def test_delete_returns_401(self): -        url = reverse('bot:reminder-detail', args=('1234',), host='api') +        url = reverse('api:bot:reminder-detail', args=('1234',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 401) -class EmptyDatabaseReminderAPITests(APISubdomainTestCase): +class EmptyDatabaseReminderAPITests(AuthenticatedAPITestCase):      def test_list_all_returns_empty_list(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), [])      def test_delete_returns_404(self): -        url = reverse('bot:reminder-detail', args=('1234',), host='api') +        url = reverse('api:bot:reminder-detail', args=('1234',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404) -class ReminderCreationTests(APISubdomainTestCase): +class ReminderCreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -64,7 +64,7 @@ class ReminderCreationTests(APISubdomainTestCase):              'channel_id': 123,              'mentions': [8888, 9999],          } -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.post(url, data=data)          self.assertEqual(response.status_code, 201)          self.assertIsNotNone(Reminder.objects.filter(id=1).first()) @@ -73,13 +73,13 @@ class ReminderCreationTests(APISubdomainTestCase):          data = {              'author': self.author.id,  # Missing multiple required fields          } -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.post(url, data=data)          self.assertEqual(response.status_code, 400)          self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=1) -class ReminderDeletionTests(APISubdomainTestCase): +class ReminderDeletionTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -97,20 +97,20 @@ class ReminderDeletionTests(APISubdomainTestCase):          )      def test_delete_unknown_reminder_returns_404(self): -        url = reverse('bot:reminder-detail', args=('something',), host='api') +        url = reverse('api:bot:reminder-detail', args=('something',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 404)      def test_delete_known_reminder_returns_204(self): -        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        url = reverse('api:bot:reminder-detail', args=(self.reminder.id,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204)          self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=self.reminder.id) -class ReminderListTests(APISubdomainTestCase): +class ReminderListTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -142,28 +142,28 @@ class ReminderListTests(APISubdomainTestCase):          cls.rem_dict_two['expiration'] += 'Z'  # Massaging a quirk of the response time format      def test_reminders_in_full_list(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two])      def test_filter_search(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.get(f'{url}?search={self.author.name}')          self.assertEqual(response.status_code, 200)          self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two])      def test_filter_field(self): -        url = reverse('bot:reminder-list', host='api') +        url = reverse('api:bot:reminder-list')          response = self.client.get(f'{url}?active=true')          self.assertEqual(response.status_code, 200)          self.assertEqual(response.json(), [self.rem_dict_one]) -class ReminderRetrieveTests(APISubdomainTestCase): +class ReminderRetrieveTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -181,17 +181,17 @@ class ReminderRetrieveTests(APISubdomainTestCase):          )      def test_retrieve_unknown_returns_404(self): -        url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') +        url = reverse('api:bot:reminder-detail', args=("not_an_id",))          response = self.client.get(url)          self.assertEqual(response.status_code, 404)      def test_retrieve_known_returns_200(self): -        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        url = reverse('api:bot:reminder-detail', args=(self.reminder.id,))          response = self.client.get(url)          self.assertEqual(response.status_code, 200) -class ReminderUpdateTests(APISubdomainTestCase): +class ReminderUpdateTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.author = User.objects.create( @@ -211,7 +211,7 @@ class ReminderUpdateTests(APISubdomainTestCase):          cls.data = {'content': 'Oops I forgot'}      def test_patch_updates_record(self): -        url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') +        url = reverse('api:bot:reminder-detail', args=(self.reminder.id,))          response = self.client.patch(url, data=self.data)          self.assertEqual(response.status_code, 200) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py index 4d1a430c..73c80c77 100644 --- a/pydis_site/apps/api/tests/test_roles.py +++ b/pydis_site/apps/api/tests/test_roles.py @@ -1,10 +1,10 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase -from ..models import Role +from .base import AuthenticatedAPITestCase +from ..models import Role, User -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.admins_role = Role.objects.create( @@ -35,6 +35,20 @@ class CreationTests(APISubdomainTestCase):              permissions=6,              position=0,          ) +        cls.role_to_delete = Role.objects.create( +            id=7, +            name="role to delete", +            colour=7, +            permissions=7, +            position=0, +        ) +        cls.role_unassigned_test_user = User.objects.create( +            id=8, +            name="role_unassigned_test_user", +            discriminator="0000", +            roles=[cls.role_to_delete.id], +            in_guild=True +        )      def _validate_roledict(self, role_dict: dict) -> None:          """Helper method to validate a dict representing a role.""" @@ -78,21 +92,21 @@ class CreationTests(APISubdomainTestCase):      def test_role_list(self):          """Tests the GET list-view and validates the contents.""" -        url = reverse('bot:role-list', host='api') +        url = reverse('api:bot:role-list')          response = self.client.get(url) -        self.assertContains(response, text="id", count=4, status_code=200) +        self.assertContains(response, text="id", count=5, status_code=200)          roles = response.json()          self.assertIsInstance(roles, list) -        self.assertEqual(len(roles), 4) +        self.assertEqual(len(roles), 5)          for role in roles:              self._validate_roledict(role)      def test_role_get_detail_success(self):          """Tests GET detail view of an existing role.""" -        url = reverse('bot:role-detail', host='api', args=(self.admins_role.id, )) +        url = reverse('api:bot:role-detail', args=(self.admins_role.id, ))          response = self.client.get(url)          self.assertContains(response, text="id", count=1, status_code=200) @@ -107,7 +121,7 @@ class CreationTests(APISubdomainTestCase):      def test_role_post_201(self):          """Tests creation of a role with a valid request.""" -        url = reverse('bot:role-list', host='api') +        url = reverse('api:bot:role-list')          data = {              "id": 1234567890,              "name": "Role Creation Test", @@ -120,7 +134,7 @@ class CreationTests(APISubdomainTestCase):      def test_role_post_invalid_request_body(self):          """Tests creation of a role with an invalid request body.""" -        url = reverse('bot:role-list', host='api') +        url = reverse('api:bot:role-list')          data = {              "name": "Role Creation Test",              "permissions": 0b01010010101, @@ -133,7 +147,7 @@ class CreationTests(APISubdomainTestCase):      def test_role_put_200(self):          """Tests PUT role request with valid request body.""" -        url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) +        url = reverse('api:bot:role-detail', args=(self.admins_role.id,))          data = {              "id": 123454321,              "name": "Role Put Alteration Test", @@ -153,7 +167,7 @@ class CreationTests(APISubdomainTestCase):      def test_role_put_invalid_request_body(self):          """Tests PUT role request with invalid request body.""" -        url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) +        url = reverse('api:bot:role-detail', args=(self.admins_role.id,))          data = {              "name": "Role Put Alteration Test",              "permissions": 255, @@ -165,7 +179,7 @@ class CreationTests(APISubdomainTestCase):      def test_role_patch_200(self):          """Tests PATCH role request with valid request body.""" -        url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) +        url = reverse('api:bot:role-detail', args=(self.admins_role.id,))          data = {              "name": "Owners"          } @@ -177,13 +191,19 @@ class CreationTests(APISubdomainTestCase):      def test_role_delete_200(self):          """Tests DELETE requests for existing role.""" -        url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) +        url = reverse('api:bot:role-detail', args=(self.admins_role.id,))          response = self.client.delete(url)          self.assertEqual(response.status_code, 204) +    def test_role_delete_unassigned(self): +        """Tests if the deleted Role gets unassigned from the user.""" +        self.role_to_delete.delete() +        self.role_unassigned_test_user.refresh_from_db() +        self.assertEqual(self.role_unassigned_test_user.roles, []) +      def test_role_detail_404_all_methods(self):          """Tests detail view with non-existing ID.""" -        url = reverse('bot:role-detail', host='api', args=(20190815,)) +        url = reverse('api:bot:role-detail', args=(20190815,))          for method in ('get', 'put', 'patch', 'delete'):              response = getattr(self.client, method)(url) diff --git a/pydis_site/apps/api/tests/test_rules.py b/pydis_site/apps/api/tests/test_rules.py index c94f89cc..d08c5fae 100644 --- a/pydis_site/apps/api/tests/test_rules.py +++ b/pydis_site/apps/api/tests/test_rules.py @@ -1,23 +1,23 @@ -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..views import RulesView -class RuleAPITests(APISubdomainTestCase): +class RuleAPITests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_can_access_rules_view(self): -        url = reverse('rules', host='api') +        url = reverse('api:rules')          response = self.client.get(url)          self.assertEqual(response.status_code, 200)          self.assertIsInstance(response.json(), list)      def test_link_format_query_param_produces_different_results(self): -        url = reverse('rules', host='api') +        url = reverse('api:rules')          markdown_links_response = self.client.get(url + '?link_format=md')          html_links_response = self.client.get(url + '?link_format=html')          self.assertNotEqual( @@ -30,6 +30,6 @@ class RuleAPITests(APISubdomainTestCase):              RulesView._format_link("a", "b", "c")      def test_get_returns_400_for_wrong_link_format(self): -        url = reverse('rules', host='api') +        url = reverse('api:rules')          response = self.client.get(url + '?link_format=unknown')          self.assertEqual(response.status_code, 400) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index c43b916a..9b91380b 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,44 +1,45 @@  from unittest.mock import patch  from django.core.exceptions import ObjectDoesNotExist -from django_hosts.resolvers import reverse +from django.urls import reverse -from .base import APISubdomainTestCase +from .base import AuthenticatedAPITestCase  from ..models import Role, User -from ..models.bot.metricity import NotFound +from ..models.bot.metricity import NotFoundError +from ..viewsets.bot.user import UserListPagination -class UnauthedUserAPITests(APISubdomainTestCase): +class UnauthedUserAPITests(AuthenticatedAPITestCase):      def setUp(self):          super().setUp()          self.client.force_authenticate(user=None)      def test_detail_lookup_returns_401(self): -        url = reverse('bot:user-detail', args=('whatever',), host='api') +        url = reverse('api:bot:user-detail', args=('whatever',))          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_list_returns_401(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          response = self.client.get(url)          self.assertEqual(response.status_code, 401)      def test_create_returns_401(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          response = self.client.post(url, data={'hi': 'there'})          self.assertEqual(response.status_code, 401)      def test_delete_returns_401(self): -        url = reverse('bot:user-detail', args=('whatever',), host='api') +        url = reverse('api:bot:user-detail', args=('whatever',))          response = self.client.delete(url)          self.assertEqual(response.status_code, 401) -class CreationTests(APISubdomainTestCase): +class CreationTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.role = Role.objects.create( @@ -57,7 +58,7 @@ class CreationTests(APISubdomainTestCase):          )      def test_accepts_valid_data(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = {              'id': 42,              'name': "Test", @@ -78,7 +79,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(user.in_guild, data['in_guild'])      def test_supports_multi_creation(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = [              {                  'id': 5, @@ -103,7 +104,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response.json(), [])      def test_returns_400_for_unknown_role_id(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = {              'id': 5,              'name': "test man", @@ -117,7 +118,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400)      def test_returns_400_for_bad_data(self): -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = {              'id': True,              'discriminator': "totally!" @@ -128,7 +129,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_user_recreation(self):          """Return 201 if User is already present in database as it skips User creation.""" -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = [{              'id': 11,              'name': 'You saw nothing.', @@ -140,7 +141,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_duplicate_request_users(self):          """Return 400 if 2 Users with same ID is passed in the request data.""" -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = [              {                  'id': 11, @@ -160,7 +161,7 @@ class CreationTests(APISubdomainTestCase):      def test_returns_400_for_existing_user(self):          """Returns 400 if user is already present in DB.""" -        url = reverse('bot:user-list', host='api') +        url = reverse('api:bot:user-list')          data = {              'id': 11,              'name': 'You saw nothing part 3.', @@ -171,7 +172,7 @@ class CreationTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400) -class MultiPatchTests(APISubdomainTestCase): +class MultiPatchTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.role_developer = Role.objects.create( @@ -195,7 +196,7 @@ class MultiPatchTests(APISubdomainTestCase):          )      def test_multiple_users_patch(self): -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  "id": 1, @@ -218,7 +219,7 @@ class MultiPatchTests(APISubdomainTestCase):          self.assertEqual(user_2.name, data[1]["name"])      def test_returns_400_for_missing_user_id(self): -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  "name": "I am ghost user!", @@ -234,7 +235,7 @@ class MultiPatchTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400)      def test_returns_404_for_not_found_user(self): -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  "id": 1, @@ -252,7 +253,7 @@ class MultiPatchTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 404)      def test_returns_400_for_bad_data(self): -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  "id": 1, @@ -268,7 +269,7 @@ class MultiPatchTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400)      def test_returns_400_for_insufficient_data(self): -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  "id": 1, @@ -282,7 +283,7 @@ class MultiPatchTests(APISubdomainTestCase):      def test_returns_400_for_duplicate_request_users(self):          """Return 400 if 2 Users with same ID is passed in the request data.""" -        url = reverse("bot:user-bulk-patch", host="api") +        url = reverse("api:bot:user-bulk-patch")          data = [              {                  'id': 1, @@ -297,7 +298,7 @@ class MultiPatchTests(APISubdomainTestCase):          self.assertEqual(response.status_code, 400) -class UserModelTests(APISubdomainTestCase): +class UserModelTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          cls.role_top = Role.objects.create( @@ -353,11 +354,11 @@ class UserModelTests(APISubdomainTestCase):          self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") -class UserPaginatorTests(APISubdomainTestCase): +class UserPaginatorTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          users = [] -        for i in range(1, 10_001): +        for i in range(1, UserListPagination.page_size + 1):              users.append(User(                  id=i,                  name=f"user{i}", @@ -367,35 +368,37 @@ class UserPaginatorTests(APISubdomainTestCase):          cls.users = User.objects.bulk_create(users)      def test_returns_single_page_response(self): -        url = reverse("bot:user-list", host="api") +        url = reverse("api:bot:user-list")          response = self.client.get(url).json()          self.assertIsNone(response["next_page_no"])          self.assertIsNone(response["previous_page_no"])      def test_returns_next_page_number(self): +        user_id = UserListPagination.page_size + 1          User.objects.create( -            id=10_001, -            name="user10001", +            id=user_id, +            name=f"user{user_id}",              discriminator=1111,              in_guild=True          ) -        url = reverse("bot:user-list", host="api") +        url = reverse("api:bot:user-list")          response = self.client.get(url).json()          self.assertEqual(2, response["next_page_no"])      def test_returns_previous_page_number(self): +        user_id = UserListPagination.page_size + 1          User.objects.create( -            id=10_001, -            name="user10001", +            id=user_id, +            name=f"user{user_id}",              discriminator=1111,              in_guild=True          ) -        url = reverse("bot:user-list", host="api") +        url = reverse("api:bot:user-list")          response = self.client.get(url, {"page": 2}).json()          self.assertEqual(1, response["previous_page_no"]) -class UserMetricityTests(APISubdomainTestCase): +class UserMetricityTests(AuthenticatedAPITestCase):      @classmethod      def setUpTestData(cls):          User.objects.create( @@ -413,12 +416,12 @@ class UserMetricityTests(APISubdomainTestCase):          self.mock_metricity_user(joined_at, total_messages, total_blocks, [])          # When -        url = reverse('bot:user-metricity-data', args=[0], host='api') +        url = reverse('api:bot:user-metricity-data', args=[0])          response = self.client.get(url)          # Then          self.assertEqual(response.status_code, 200) -        self.assertEqual(response.json(), { +        self.assertCountEqual(response.json(), {              "joined_at": joined_at,              "total_messages": total_messages,              "voice_banned": False, @@ -430,7 +433,7 @@ class UserMetricityTests(APISubdomainTestCase):          self.mock_no_metricity_user()          # When -        url = reverse('bot:user-metricity-data', args=[0], host='api') +        url = reverse('api:bot:user-metricity-data', args=[0])          response = self.client.get(url)          # Then @@ -441,7 +444,7 @@ class UserMetricityTests(APISubdomainTestCase):          self.mock_no_metricity_user()          # When -        url = reverse('bot:user-metricity-review-data', args=[0], host='api') +        url = reverse('api:bot:user-metricity-review-data', args=[0])          response = self.client.get(url)          # Then @@ -460,7 +463,7 @@ class UserMetricityTests(APISubdomainTestCase):                  with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p:                      p.side_effect = case['exception'] -                    url = reverse('bot:user-metricity-data', args=[0], host='api') +                    url = reverse('api:bot:user-metricity-data', args=[0])                      response = self.client.get(url)                      self.assertEqual(response.status_code, 200) @@ -475,7 +478,7 @@ class UserMetricityTests(APISubdomainTestCase):          self.mock_metricity_user(joined_at, total_messages, total_blocks, channel_activity)          # When -        url = reverse('bot:user-metricity-review-data', args=[0], host='api') +        url = reverse('api:bot:user-metricity-review-data', args=[0])          response = self.client.get(url)          # Then @@ -501,7 +504,7 @@ class UserMetricityTests(APISubdomainTestCase):          self.metricity = patcher.start()          self.addCleanup(patcher.stop)          self.metricity = self.metricity.return_value.__enter__.return_value -        self.metricity.user.side_effect = NotFound() -        self.metricity.total_messages.side_effect = NotFound() -        self.metricity.total_message_blocks.side_effect = NotFound() -        self.metricity.top_channel_activity.side_effect = NotFound() +        self.metricity.user.side_effect = NotFoundError() +        self.metricity.total_messages.side_effect = NotFoundError() +        self.metricity.total_message_blocks.side_effect = NotFoundError() +        self.metricity.top_channel_activity.side_effect = NotFoundError() diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 2e1ef0b4..b0ab545b 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -16,7 +16,7 @@ from .viewsets import (      UserViewSet  ) -# http://www.django-rest-framework.org/api-guide/routers/#defaultrouter +# https://www.django-rest-framework.org/api-guide/routers/#defaultrouter  bot_router = DefaultRouter(trailing_slash=False)  bot_router.register(      'filter-lists', diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py index 2cb21ab9..4b05acee 100644 --- a/pydis_site/apps/api/viewsets/bot/filter_list.py +++ b/pydis_site/apps/api/viewsets/bot/filter_list.py @@ -59,7 +59,8 @@ class FilterListViewSet(ModelViewSet):      ...     ["GUILD_INVITE","Guild Invite"],      ...     ["FILE_FORMAT","File Format"],      ...     ["DOMAIN_NAME","Domain Name"], -    ...     ["FILTER_TOKEN","Filter Token"] +    ...     ["FILTER_TOKEN","Filter Token"], +    ...     ["REDIRECT", "Redirect"]      ... ]      #### Status codes diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index f8b0cb9d..8a48ed1f 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -70,7 +70,8 @@ class InfractionViewSet(      ...         'actor': 125435062127820800,      ...         'type': 'ban',      ...         'reason': 'He terk my jerb!', -    ...         'hidden': True +    ...         'hidden': True, +    ...         'dm_sent': True      ...     }      ... ] @@ -100,7 +101,8 @@ class InfractionViewSet(      ...     'hidden': True,      ...     'type': 'ban',      ...     'reason': 'He terk my jerb!', -    ...     'user': 172395097705414656 +    ...     'user': 172395097705414656, +    ...     'dm_sent': False      ... }      #### Response format @@ -118,7 +120,8 @@ class InfractionViewSet(      >>> {      ...     'active': True,      ...     'expires_at': '4143-02-15T21:04:31+00:00', -    ...     'reason': 'durka derr' +    ...     'reason': 'durka derr', +    ...     'dm_sent': True      ... }      #### Response format diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 826ad25e..78f8c340 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,18 +1,17 @@  from django.db.models import Case, Value, When  from django.db.models.query import QuerySet -from django.http.request import HttpRequest  from django.shortcuts import get_object_or_404  from rest_framework.exceptions import ParseError -from rest_framework.mixins import DestroyModelMixin +from rest_framework.request import Request  from rest_framework.response import Response  from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import ViewSet +from rest_framework.viewsets import ModelViewSet  from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName  from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer -class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): +class OffTopicChannelNameViewSet(ModelViewSet):      """      View of off-topic channel names used by the bot to rotate our off-topic names on a daily basis. @@ -20,7 +19,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):      ### GET /bot/off-topic-channel-names      Return all known off-topic channel names from the database.      If the `random_items` query parameter is given, for example using... -        $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 +        $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?random_items=5      ... then the API will return `5` random items from the database      that is not used in current rotation.      When running out of names, API will mark all names to not used and start new rotation. @@ -39,7 +38,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):      ### POST /bot/off-topic-channel-names      Create a new off-topic-channel name in the database.      The name must be given as a query parameter, for example: -        $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?name=lemons-lemonade-shop +        $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?name=lemons-lemonade-shop      #### Status codes      - 201: returned on success @@ -58,6 +57,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):      lookup_field = 'name'      serializer_class = OffTopicChannelNameSerializer +    queryset = OffTopicChannelName.objects.all()      def get_object(self) -> OffTopicChannelName:          """ @@ -65,15 +65,14 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):          If it doesn't, a HTTP 404 is returned by way of throwing an exception.          """ -        queryset = self.get_queryset()          name = self.kwargs[self.lookup_field] -        return get_object_or_404(queryset, name=name) +        return get_object_or_404(self.queryset, name=name)      def get_queryset(self) -> QuerySet:          """Returns a queryset that covers the entire OffTopicChannelName table."""          return OffTopicChannelName.objects.all() -    def create(self, request: HttpRequest) -> Response: +    def create(self, request: Request, *args, **kwargs) -> Response:          """          DRF method for creating a new OffTopicChannelName. @@ -91,7 +90,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):                  'name': ["This query parameter is required."]              }) -    def list(self, request: HttpRequest) -> Response: +    def list(self, request: Request, *args, **kwargs) -> Response:          """          DRF method for listing OffTopicChannelName entries. @@ -109,13 +108,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):                      'random_items': ["Must be a positive integer."]                  }) -            queryset = self.get_queryset().order_by('used', '?')[:random_count] +            queryset = self.queryset.order_by('used', '?')[:random_count]              # When any name is used in our listing then this means we reached end of round              # and we need to reset all other names `used` to False              if any(offtopic_name.used for offtopic_name in queryset):                  # These names that we just got have to be excluded from updating used to False -                self.get_queryset().update( +                self.queryset.update(                      used=Case(                          When(                              name__in=(offtopic_name.name for offtopic_name in queryset), @@ -126,13 +125,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):                  )              else:                  # Otherwise mark selected names `used` to True -                self.get_queryset().filter( +                self.queryset.filter(                      name__in=(offtopic_name.name for offtopic_name in queryset)                  ).update(used=True)              serialized = self.serializer_class(queryset, many=True)              return Response(serialized.data) -        queryset = self.get_queryset() +        params = {} + +        if active_param := request.query_params.get("active"): +            params["active"] = active_param.lower() == "true" + +        queryset = self.queryset.filter(**params)          serialized = self.serializer_class(queryset, many=True)          return Response(serialized.data) diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 111660d9..78d7cb3b 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -42,7 +42,8 @@ class ReminderViewSet(      ...         'expiration': '5018-11-20T15:52:00Z',      ...         'id': 11,      ...         'channel_id': 634547009956872193, -    ...         'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>" +    ...         'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", +    ...         'failures': 3      ...     },      ...     ...      ... ] @@ -67,7 +68,8 @@ class ReminderViewSet(      ...     'expiration': '5018-11-20T15:52:00Z',      ...     'id': 11,      ...     'channel_id': 634547009956872193, -    ...     'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>" +    ...     'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", +    ...     'failures': 3      ... }      #### Status codes @@ -80,7 +82,7 @@ class ReminderViewSet(      #### Request body      >>> {      ...     'author': int, -    ...     'mentions': List[int], +    ...     'mentions': list[int],      ...     'content': str,      ...     'expiration': str,  # ISO-formatted datetime      ...     'channel_id': int, @@ -98,9 +100,10 @@ class ReminderViewSet(      #### Request body      >>> { -    ...     'mentions': List[int], +    ...     'mentions': list[int],      ...     'content': str, -    ...     'expiration': str  # ISO-formatted datetime +    ...     'expiration': str,  # ISO-formatted datetime +    ...     'failures': int      ... }      #### Status codes diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 25722f5a..1a5e79f8 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -11,7 +11,7 @@ from rest_framework.serializers import ModelSerializer  from rest_framework.viewsets import ModelViewSet  from pydis_site.apps.api.models.bot.infraction import Infraction -from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound +from pydis_site.apps.api.models.bot.metricity import Metricity, NotFoundError  from pydis_site.apps.api.models.bot.user import User  from pydis_site.apps.api.serializers import UserSerializer @@ -19,7 +19,7 @@ from pydis_site.apps.api.serializers import UserSerializer  class UserListPagination(PageNumberPagination):      """Custom pagination class for the User Model.""" -    page_size = 10000 +    page_size = 2500      page_size_query_param = "page_size"      def get_next_page_number(self) -> typing.Optional[int]: @@ -271,11 +271,13 @@ class UserViewSet(ModelViewSet):          with Metricity() as metricity:              try:                  data = metricity.user(user.id) +                  data["total_messages"] = metricity.total_messages(user.id) -                data["voice_banned"] = voice_banned                  data["activity_blocks"] = metricity.total_message_blocks(user.id) + +                data["voice_banned"] = voice_banned                  return Response(data, status=status.HTTP_200_OK) -            except NotFound: +            except NotFoundError:                  return Response(dict(detail="User not found in metricity"),                                  status=status.HTTP_404_NOT_FOUND) @@ -290,6 +292,6 @@ class UserViewSet(ModelViewSet):                  data["total_messages"] = metricity.total_messages(user.id)                  data["top_channel_activity"] = metricity.top_channel_activity(user.id)                  return Response(data, status=status.HTTP_200_OK) -            except NotFound: +            except NotFoundError:                  return Response(dict(detail="User not found in metricity"),                                  status=status.HTTP_404_NOT_FOUND)  |