aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api/tests
diff options
context:
space:
mode:
authorGravatar scragly <[email protected]>2020-09-18 03:33:35 +1000
committerGravatar scragly <[email protected]>2020-09-18 03:33:35 +1000
commit357b20145b2784d9334b941fc25bcb8ce7b64c11 (patch)
treeccd446b4841ad69ae22fdc91f4c246823e0f811e /pydis_site/apps/api/tests
parentAdd new test for deleted message context log_url. (diff)
parentMerge pull request #390 from python-discord/allow_blank_or_null_for_nominatio... (diff)
Merge branch 'master' into admin-api-pages-improvements
# Conflicts: # pydis_site/apps/api/admin.py
Diffstat (limited to 'pydis_site/apps/api/tests')
-rw-r--r--pydis_site/apps/api/tests/base.py2
-rw-r--r--pydis_site/apps/api/tests/migrations/__init__.py1
-rw-r--r--pydis_site/apps/api/tests/migrations/base.py102
-rw-r--r--pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py496
-rw-r--r--pydis_site/apps/api/tests/migrations/test_base.py135
-rw-r--r--pydis_site/apps/api/tests/test_deleted_messages.py15
-rw-r--r--pydis_site/apps/api/tests/test_documentation_links.py4
-rw-r--r--pydis_site/apps/api/tests/test_filterlists.py122
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py321
-rw-r--r--pydis_site/apps/api/tests/test_models.py72
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py13
-rw-r--r--pydis_site/apps/api/tests/test_off_topic_channel_names.py31
-rw-r--r--pydis_site/apps/api/tests/test_offensive_message.py155
-rw-r--r--pydis_site/apps/api/tests/test_reminders.py221
-rw-r--r--pydis_site/apps/api/tests/test_roles.py2
-rw-r--r--pydis_site/apps/api/tests/test_users.py14
-rw-r--r--pydis_site/apps/api/tests/test_validators.py68
17 files changed, 1666 insertions, 108 deletions
diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py
index b779256e..61c23b0f 100644
--- a/pydis_site/apps/api/tests/base.py
+++ b/pydis_site/apps/api/tests/base.py
@@ -5,7 +5,7 @@ from rest_framework.test import APITestCase
test_user, _created = User.objects.get_or_create(
username='test',
- password='testpass', # noqa
+ password='testpass',
is_superuser=True,
is_staff=True
)
diff --git a/pydis_site/apps/api/tests/migrations/__init__.py b/pydis_site/apps/api/tests/migrations/__init__.py
new file mode 100644
index 00000000..38e42ffc
--- /dev/null
+++ b/pydis_site/apps/api/tests/migrations/__init__.py
@@ -0,0 +1 @@
+"""This submodule contains tests for functions used in data migrations."""
diff --git a/pydis_site/apps/api/tests/migrations/base.py b/pydis_site/apps/api/tests/migrations/base.py
new file mode 100644
index 00000000..0c0a5bd0
--- /dev/null
+++ b/pydis_site/apps/api/tests/migrations/base.py
@@ -0,0 +1,102 @@
+"""Includes utilities for testing migrations."""
+from django.db import connection
+from django.db.migrations.executor import MigrationExecutor
+from django.test import TestCase
+
+
+class MigrationsTestCase(TestCase):
+ """
+ A `TestCase` subclass to test migration files.
+
+ To be able to properly test a migration, we will need to inject data into the test database
+ before the migrations we want to test are applied, but after the older migrations have been
+ applied. This makes sure that we are testing "as if" we were actually applying this migration
+ to a database in the state it was in before introducing the new migration.
+
+ To set up a MigrationsTestCase, create a subclass of this class and set the following
+ class-level attributes:
+
+ - app: The name of the app that contains the migrations (e.g., `'api'`)
+ - migration_prior: The name* of the last migration file before the migrations you want to test
+ - migration_target: The name* of the last migration file we want to test
+
+ *) Specify the file names without a path or the `.py` file extension.
+
+ Additionally, overwrite the `setUpMigrationData` in the subclass to inject data into the
+ database before the migrations we want to test are applied. Please read the docstring of the
+ method for more information. An optional hook, `setUpPostMigrationData` is also provided.
+ """
+
+ # These class-level attributes should be set in classes that inherit from this base class.
+ app = None
+ migration_prior = None
+ migration_target = None
+
+ @classmethod
+ def setUpTestData(cls):
+ """
+ Injects data into the test database prior to the migration we're trying to test.
+
+ This class methods reverts the test database back to the state of the last migration file
+ prior to the migrations we want to test. It will then allow the user to inject data into the
+ test database by calling the `setUpMigrationData` hook. After the data has been injected, it
+ will apply the migrations we want to test and call the `setUpPostMigrationData` hook. The
+ user can now test if the migration correctly migrated the injected test data.
+ """
+ if not cls.app:
+ raise ValueError("The `app` attribute was not set.")
+
+ if not cls.migration_prior or not cls.migration_target:
+ raise ValueError("Both ` migration_prior` and `migration_target` need to be set.")
+
+ cls.migrate_from = [(cls.app, cls.migration_prior)]
+ cls.migrate_to = [(cls.app, cls.migration_target)]
+
+ # Reverse to database state prior to the migrations we want to test
+ executor = MigrationExecutor(connection)
+ executor.migrate(cls.migrate_from)
+
+ # Call the data injection hook with the current state of the project
+ old_apps = executor.loader.project_state(cls.migrate_from).apps
+ cls.setUpMigrationData(old_apps)
+
+ # Run the migrations we want to test
+ executor = MigrationExecutor(connection)
+ executor.loader.build_graph()
+ executor.migrate(cls.migrate_to)
+
+ # Save the project state so we're able to work with the correct model states
+ cls.apps = executor.loader.project_state(cls.migrate_to).apps
+
+ # Call `setUpPostMigrationData` to potentially set up post migration data used in testing
+ cls.setUpPostMigrationData(cls.apps)
+
+ @classmethod
+ def setUpMigrationData(cls, apps):
+ """
+ Override this method to inject data into the test database before the migration is applied.
+
+ This method will be called after setting up the database according to the migrations that
+ come before the migration(s) we are trying to test, but before the to-be-tested migration(s)
+ are applied. This allows us to simulate a database state just prior to the migrations we are
+ trying to test.
+
+ To make sure we're creating objects according to the state the models were in at this point
+ in the migration history, use `apps.get_model(app_name: str, model_name: str)` to get the
+ appropriate model, e.g.:
+
+ >>> Infraction = apps.get_model('api', 'Infraction')
+ """
+ pass
+
+ @classmethod
+ def setUpPostMigrationData(cls, apps):
+ """
+ Set up additional test data after the target migration has been applied.
+
+ Use `apps.get_model(app_name: str, model_name: str)` to get the correct instances of the
+ model classes:
+
+ >>> Infraction = apps.get_model('api', 'Infraction')
+ """
+ pass
diff --git a/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py
new file mode 100644
index 00000000..8dc29b34
--- /dev/null
+++ b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py
@@ -0,0 +1,496 @@
+"""Tests for the data migration in `filename`."""
+import logging
+from collections import ChainMap, namedtuple
+from datetime import timedelta
+from itertools import count
+from typing import Dict, Iterable, Type, Union
+
+from django.db.models import Q
+from django.forms.models import model_to_dict
+from django.utils import timezone
+
+from pydis_site.apps.api.models import Infraction, User
+from .base import MigrationsTestCase
+
+log = logging.getLogger(__name__)
+log.setLevel(logging.DEBUG)
+
+
+InfractionHistory = namedtuple('InfractionHistory', ("user_id", "infraction_history"))
+
+
+class InfractionFactory:
+ """Factory that creates infractions for a User instance."""
+
+ infraction_id = count(1)
+ user_id = count(1)
+ default_values = {
+ 'active': True,
+ 'expires_at': None,
+ 'hidden': False,
+ }
+
+ @classmethod
+ def create(
+ cls,
+ actor: User,
+ infractions: Iterable[Dict[str, Union[str, int, bool]]],
+ infraction_model: Type[Infraction] = Infraction,
+ user_model: Type[User] = User,
+ ) -> InfractionHistory:
+ """
+ Creates `infractions` for the `user` with the given `actor`.
+
+ The `infractions` dictionary can contain the following fields:
+ - `type` (required)
+ - `active` (default: True)
+ - `expires_at` (default: None; i.e, permanent)
+ - `hidden` (default: False).
+
+ The parameters `infraction_model` and `user_model` can be used to pass in an instance of
+ both model classes from a different migration/project state.
+ """
+ user_id = next(cls.user_id)
+ user = user_model.objects.create(
+ id=user_id,
+ name=f"Infracted user {user_id}",
+ discriminator=user_id,
+ avatar_hash=None,
+ )
+ infraction_history = []
+
+ for infraction in infractions:
+ infraction = dict(infraction)
+ infraction["id"] = next(cls.infraction_id)
+ infraction = ChainMap(infraction, cls.default_values)
+ new_infraction = infraction_model.objects.create(
+ user=user,
+ actor=actor,
+ type=infraction["type"],
+ reason=f"`{infraction['type']}` infraction (ID: {infraction['id']} of {user}",
+ active=infraction['active'],
+ hidden=infraction['hidden'],
+ expires_at=infraction['expires_at'],
+ )
+ infraction_history.append(new_infraction)
+
+ return InfractionHistory(user_id=user_id, infraction_history=infraction_history)
+
+
+class InfractionFactoryTests(MigrationsTestCase):
+ """Tests for the InfractionFactory."""
+
+ app = "api"
+ migration_prior = "0046_reminder_jump_url"
+ migration_target = "0046_reminder_jump_url"
+
+ @classmethod
+ def setUpPostMigrationData(cls, apps):
+ """Create a default actor for all infractions."""
+ cls.infraction_model = apps.get_model('api', 'Infraction')
+ cls.user_model = apps.get_model('api', 'User')
+
+ cls.actor = cls.user_model.objects.create(
+ id=9999,
+ name="Unknown Moderator",
+ discriminator=1040,
+ avatar_hash=None,
+ )
+
+ def test_infraction_factory_total_count(self):
+ """Does the test database hold as many infractions as we tried to create?"""
+ InfractionFactory.create(
+ actor=self.actor,
+ infractions=(
+ {'type': 'kick', 'active': False, 'hidden': False},
+ {'type': 'ban', 'active': True, 'hidden': False},
+ {'type': 'note', 'active': False, 'hidden': True},
+ ),
+ infraction_model=self.infraction_model,
+ user_model=self.user_model,
+ )
+ database_count = Infraction.objects.all().count()
+ self.assertEqual(3, database_count)
+
+ def test_infraction_factory_multiple_users(self):
+ """Does the test database hold as many infractions as we tried to create?"""
+ for _user in range(5):
+ InfractionFactory.create(
+ actor=self.actor,
+ infractions=(
+ {'type': 'kick', 'active': False, 'hidden': True},
+ {'type': 'ban', 'active': True, 'hidden': False},
+ ),
+ infraction_model=self.infraction_model,
+ user_model=self.user_model,
+ )
+
+ # Check if infractions and users are recorded properly in the database
+ database_count = Infraction.objects.all().count()
+ self.assertEqual(database_count, 10)
+
+ user_count = User.objects.all().count()
+ self.assertEqual(user_count, 5 + 1)
+
+ def test_infraction_factory_sets_correct_fields(self):
+ """Does the InfractionFactory set the correct attributes?"""
+ infractions = (
+ {
+ 'type': 'note',
+ 'active': False,
+ 'hidden': True,
+ 'expires_at': timezone.now()
+ },
+ {'type': 'warning', 'active': False, 'hidden': False, 'expires_at': None},
+ {'type': 'watch', 'active': False, 'hidden': True, 'expires_at': None},
+ {'type': 'mute', 'active': True, 'hidden': False, 'expires_at': None},
+ {'type': 'kick', 'active': True, 'hidden': True, 'expires_at': None},
+ {'type': 'ban', 'active': True, 'hidden': False, 'expires_at': None},
+ {
+ 'type': 'superstar',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now()
+ },
+ )
+
+ InfractionFactory.create(
+ actor=self.actor,
+ infractions=infractions,
+ infraction_model=self.infraction_model,
+ user_model=self.user_model,
+ )
+
+ for infraction in infractions:
+ with self.subTest(**infraction):
+ self.assertTrue(Infraction.objects.filter(**infraction).exists())
+
+
+class ActiveInfractionMigrationTests(MigrationsTestCase):
+ """
+ Tests the active infraction data migration.
+
+ The active infraction data migration should do the following things:
+
+ 1. migrates all active notes, warnings, and kicks to an inactive status;
+ 2. migrates all users with multiple active infractions of a single type to have only one active
+ infraction of that type. The infraction with the longest duration stays active.
+ """
+
+ app = "api"
+ migration_prior = "0046_reminder_jump_url"
+ migration_target = "0047_active_infractions_migration"
+
+ @classmethod
+ def setUpMigrationData(cls, apps):
+ """Sets up an initial database state that contains the relevant test cases."""
+ # Fetch the Infraction and User model in the current migration state
+ cls.infraction_model = apps.get_model('api', 'Infraction')
+ cls.user_model = apps.get_model('api', 'User')
+
+ cls.created_infractions = {}
+
+ # Moderator that serves as actor for all infractions
+ cls.user_moderator = cls.user_model.objects.create(
+ id=9999,
+ name="Olivier de Vienne",
+ discriminator=1040,
+ avatar_hash=None,
+ )
+
+ # User #1: clean user with no infractions
+ cls.created_infractions["no infractions"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=[],
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #2: One inactive note infraction
+ cls.created_infractions["one inactive note"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'note', 'active': False, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #3: One active note infraction
+ cls.created_infractions["one active note"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'note', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #4: One active and one inactive note infraction
+ cls.created_infractions["one active and one inactive note"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'note', 'active': False, 'hidden': True},
+ {'type': 'note', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #5: Once active note, one active kick, once active warning
+ cls.created_infractions["active note, kick, warning"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'note', 'active': True, 'hidden': True},
+ {'type': 'kick', 'active': True, 'hidden': True},
+ {'type': 'warning', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #6: One inactive ban and one active ban
+ cls.created_infractions["one inactive and one active ban"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'ban', 'active': False, 'hidden': True},
+ {'type': 'ban', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #7: Two active permanent bans
+ cls.created_infractions["two active perm bans"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'ban', 'active': True, 'hidden': True},
+ {'type': 'ban', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #8: Multiple active temporary bans
+ cls.created_infractions["multiple active temp bans"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=1)
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=10)
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=20)
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=5)
+ },
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #9: One active permanent ban, two active temporary bans
+ cls.created_infractions["active perm, two active temp bans"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=10)
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': None,
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=7)
+ },
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #10: One inactive permanent ban, two active temporary bans
+ cls.created_infractions["one inactive perm ban, two active temp bans"] = (
+ InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=10)
+ },
+ {
+ 'type': 'ban',
+ 'active': False,
+ 'hidden': True,
+ 'expires_at': None,
+ },
+ {
+ 'type': 'ban',
+ 'active': True,
+ 'hidden': True,
+ 'expires_at': timezone.now() + timedelta(days=7)
+ },
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+ )
+
+ # User #11: Active ban, active mute, active superstar
+ cls.created_infractions["active ban, mute, and superstar"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'ban', 'active': True, 'hidden': True},
+ {'type': 'mute', 'active': True, 'hidden': True},
+ {'type': 'superstar', 'active': True, 'hidden': True},
+ {'type': 'watch', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ # User #12: Multiple active bans, active mutes, active superstars
+ cls.created_infractions["multiple active bans, mutes, stars"] = InfractionFactory.create(
+ actor=cls.user_moderator,
+ infractions=(
+ {'type': 'ban', 'active': True, 'hidden': True},
+ {'type': 'ban', 'active': True, 'hidden': True},
+ {'type': 'ban', 'active': True, 'hidden': True},
+ {'type': 'mute', 'active': True, 'hidden': True},
+ {'type': 'mute', 'active': True, 'hidden': True},
+ {'type': 'mute', 'active': True, 'hidden': True},
+ {'type': 'superstar', 'active': True, 'hidden': True},
+ {'type': 'superstar', 'active': True, 'hidden': True},
+ {'type': 'superstar', 'active': True, 'hidden': True},
+ {'type': 'watch', 'active': True, 'hidden': True},
+ {'type': 'watch', 'active': True, 'hidden': True},
+ {'type': 'watch', 'active': True, 'hidden': True},
+ ),
+ infraction_model=cls.infraction_model,
+ user_model=cls.user_model,
+ )
+
+ def test_all_never_active_types_became_inactive(self):
+ """Are all infractions of a non-active type inactive after the migration?"""
+ inactive_type_query = Q(type="note") | Q(type="warning") | Q(type="kick")
+ self.assertFalse(
+ self.infraction_model.objects.filter(inactive_type_query, active=True).exists()
+ )
+
+ def test_migration_left_clean_user_without_infractions(self):
+ """Do users without infractions have no infractions after the migration?"""
+ user_id, infraction_history = self.created_infractions["no infractions"]
+ self.assertFalse(
+ self.infraction_model.objects.filter(user__id=user_id).exists()
+ )
+
+ def test_migration_left_user_with_inactive_note_untouched(self):
+ """Did the migration leave users with only an inactive note untouched?"""
+ user_id, infraction_history = self.created_infractions["one inactive note"]
+ inactive_note = infraction_history[0]
+ self.assertTrue(
+ self.infraction_model.objects.filter(**model_to_dict(inactive_note)).exists()
+ )
+
+ def test_migration_only_touched_active_field_of_active_note(self):
+ """Does the migration only change the `active` field?"""
+ user_id, infraction_history = self.created_infractions["one active note"]
+ note = model_to_dict(infraction_history[0])
+ note['active'] = False
+ self.assertTrue(
+ self.infraction_model.objects.filter(**note).exists()
+ )
+
+ def test_migration_only_touched_active_field_of_active_note_left_inactive_untouched(self):
+ """Does the migration only change the `active` field of active notes?"""
+ user_id, infraction_history = self.created_infractions["one active and one inactive note"]
+ for note in infraction_history:
+ with self.subTest(active=note.active):
+ note = model_to_dict(note)
+ note['active'] = False
+ self.assertTrue(
+ self.infraction_model.objects.filter(**note).exists()
+ )
+
+ def test_migration_migrates_all_nonactive_types_to_inactive(self):
+ """Do we set the `active` field of all non-active infractions to `False`?"""
+ user_id, infraction_history = self.created_infractions["active note, kick, warning"]
+ self.assertFalse(
+ self.infraction_model.objects.filter(user__id=user_id, active=True).exists()
+ )
+
+ def test_migration_leaves_user_with_one_active_ban_untouched(self):
+ """Do we leave a user with one active and one inactive ban untouched?"""
+ user_id, infraction_history = self.created_infractions["one inactive and one active ban"]
+ for infraction in infraction_history:
+ with self.subTest(active=infraction.active):
+ self.assertTrue(
+ self.infraction_model.objects.filter(**model_to_dict(infraction)).exists()
+ )
+
+ def test_migration_turns_double_active_perm_ban_into_single_active_perm_ban(self):
+ """Does the migration turn two active permanent bans into one active permanent ban?"""
+ user_id, infraction_history = self.created_infractions["two active perm bans"]
+ active_count = self.infraction_model.objects.filter(user__id=user_id, active=True).count()
+ self.assertEqual(active_count, 1)
+
+ def test_migration_leaves_temporary_ban_with_longest_duration_active(self):
+ """Does the migration turn two active permanent bans into one active permanent ban?"""
+ user_id, infraction_history = self.created_infractions["multiple active temp bans"]
+ active_ban = self.infraction_model.objects.get(user__id=user_id, active=True)
+ self.assertEqual(active_ban.expires_at, infraction_history[2].expires_at)
+
+ def test_migration_leaves_permanent_ban_active(self):
+ """Does the migration leave the permanent ban active?"""
+ user_id, infraction_history = self.created_infractions["active perm, two active temp bans"]
+ active_ban = self.infraction_model.objects.get(user__id=user_id, active=True)
+ self.assertIsNone(active_ban.expires_at)
+
+ def test_migration_leaves_longest_temp_ban_active_with_inactive_permanent_ban(self):
+ """Does the longest temp ban stay active, even with an inactive perm ban present?"""
+ user_id, infraction_history = self.created_infractions[
+ "one inactive perm ban, two active temp bans"
+ ]
+ active_ban = self.infraction_model.objects.get(user__id=user_id, active=True)
+ self.assertEqual(active_ban.expires_at, infraction_history[0].expires_at)
+
+ def test_migration_leaves_all_active_types_active_if_one_of_each_exists(self):
+ """Do all active infractions stay active if only one of each is present?"""
+ user_id, infraction_history = self.created_infractions["active ban, mute, and superstar"]
+ active_count = self.infraction_model.objects.filter(user__id=user_id, active=True).count()
+ self.assertEqual(active_count, 4)
+
+ def test_migration_reduces_all_active_types_to_a_single_active_infraction(self):
+ """Do we reduce all of the infraction types to one active infraction?"""
+ user_id, infraction_history = self.created_infractions["multiple active bans, mutes, stars"]
+ active_infractions = self.infraction_model.objects.filter(user__id=user_id, active=True)
+ self.assertEqual(len(active_infractions), 4)
+ types_observed = [infraction.type for infraction in active_infractions]
+
+ for infraction_type in ('ban', 'mute', 'superstar', 'watch'):
+ with self.subTest(type=infraction_type):
+ self.assertIn(infraction_type, types_observed)
diff --git a/pydis_site/apps/api/tests/migrations/test_base.py b/pydis_site/apps/api/tests/migrations/test_base.py
new file mode 100644
index 00000000..f69bc92c
--- /dev/null
+++ b/pydis_site/apps/api/tests/migrations/test_base.py
@@ -0,0 +1,135 @@
+import logging
+from unittest.mock import call, patch
+
+from django.db.migrations.loader import MigrationLoader
+from django.test import TestCase
+
+from .base import MigrationsTestCase, connection
+
+log = logging.getLogger(__name__)
+
+
+class SpanishInquisition(MigrationsTestCase):
+ app = "api"
+ migration_prior = "scragly"
+ migration_target = "kosa"
+
+
+@patch("pydis_site.apps.api.tests.migrations.base.MigrationExecutor")
+class MigrationsTestCaseNoSideEffectsTests(TestCase):
+ """Tests the MigrationTestCase class with actual migration side effects disabled."""
+
+ def setUp(self):
+ """Set up an instance of MigrationsTestCase for use in tests."""
+ self.test_case = SpanishInquisition()
+
+ def test_missing_app_class_raises_value_error(self, _migration_executor):
+ """A MigrationsTestCase subclass should set the class-attribute `app`."""
+ class Spam(MigrationsTestCase):
+ pass
+
+ spam = Spam()
+ with self.assertRaises(ValueError, msg="The `app` attribute was not set."):
+ spam.setUpTestData()
+
+ def test_missing_migration_class_attributes_raise_value_error(self, _migration_executor):
+ """A MigrationsTestCase subclass should set both `migration_prior` and `migration_target`"""
+ class Eggs(MigrationsTestCase):
+ app = "api"
+ migration_target = "lemon"
+
+ class Bacon(MigrationsTestCase):
+ app = "api"
+ migration_prior = "mark"
+
+ instances = (Eggs(), Bacon())
+
+ exception_message = "Both ` migration_prior` and `migration_target` need to be set."
+ for instance in instances:
+ with self.subTest(
+ migration_prior=instance.migration_prior,
+ migration_target=instance.migration_target,
+ ):
+ with self.assertRaises(ValueError, msg=exception_message):
+ instance.setUpTestData()
+
+ @patch(f"{__name__}.SpanishInquisition.setUpMigrationData")
+ @patch(f"{__name__}.SpanishInquisition.setUpPostMigrationData")
+ def test_migration_data_hooks_are_called_once(self, pre_hook, post_hook, _migration_executor):
+ """The `setUpMigrationData` and `setUpPostMigrationData` hooks should be called once."""
+ self.test_case.setUpTestData()
+ for hook in (pre_hook, post_hook):
+ with self.subTest(hook=repr(hook)):
+ hook.assert_called_once()
+
+ def test_migration_executor_is_instantiated_twice(self, migration_executor):
+ """The `MigrationExecutor` should be instantiated with the database connection twice."""
+ self.test_case.setUpTestData()
+
+ expected_args = [call(connection), call(connection)]
+ self.assertEqual(migration_executor.call_args_list, expected_args)
+
+ def test_project_state_is_loaded_for_correct_migration_files_twice(self, migration_executor):
+ """The `project_state` should first be loaded with `migrate_from`, then `migrate_to`."""
+ self.test_case.setUpTestData()
+
+ expected_args = [call(self.test_case.migrate_from), call(self.test_case.migrate_to)]
+ self.assertEqual(migration_executor().loader.project_state.call_args_list, expected_args)
+
+ def test_loader_build_graph_gets_called_once(self, migration_executor):
+ """We should rebuild the migration graph before applying the second set of migrations."""
+ self.test_case.setUpTestData()
+
+ migration_executor().loader.build_graph.assert_called_once()
+
+ def test_migration_executor_migrate_method_is_called_correctly_twice(self, migration_executor):
+ """The migrate method of the executor should be called twice with the correct arguments."""
+ self.test_case.setUpTestData()
+
+ self.assertEqual(migration_executor().migrate.call_count, 2)
+ calls = [call([('api', 'scragly')]), call([('api', 'kosa')])]
+ migration_executor().migrate.assert_has_calls(calls)
+
+
+class LifeOfBrian(MigrationsTestCase):
+ app = "api"
+ migration_prior = "0046_reminder_jump_url"
+ migration_target = "0048_add_infractions_unique_constraints_active"
+
+ @classmethod
+ def log_last_migration(cls):
+ """Parses the applied migrations dictionary to log the last applied migration."""
+ loader = MigrationLoader(connection)
+ api_migrations = [
+ migration for app, migration in loader.applied_migrations if app == cls.app
+ ]
+ last_migration = max(api_migrations, key=lambda name: int(name[:4]))
+ log.info(f"The last applied migration: {last_migration}")
+
+ @classmethod
+ def setUpMigrationData(cls, apps):
+ """Method that logs the last applied migration at this point."""
+ cls.log_last_migration()
+
+ @classmethod
+ def setUpPostMigrationData(cls, apps):
+ """Method that logs the last applied migration at this point."""
+ cls.log_last_migration()
+
+
+class MigrationsTestCaseMigrationTest(TestCase):
+ """Tests if `MigrationsTestCase` travels to the right points in the migration history."""
+
+ def test_migrations_test_case_travels_to_correct_migrations_in_history(self):
+ """The test case should first revert to `migration_prior`, then go to `migration_target`."""
+ brian = LifeOfBrian()
+
+ with self.assertLogs(log, level=logging.INFO) as logs:
+ brian.setUpTestData()
+
+ self.assertEqual(len(logs.records), 2)
+
+ for time_point, record in zip(("migration_prior", "migration_target"), logs.records):
+ with self.subTest(time_point=time_point):
+ message = f"The last applied migration: {getattr(brian, time_point)}"
+ self.assertEqual(record.getMessage(), message)
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
index ccccdda4..287c1737 100644
--- a/pydis_site/apps/api/tests/test_deleted_messages.py
+++ b/pydis_site/apps/api/tests/test_deleted_messages.py
@@ -9,12 +9,11 @@ from ..models import MessageDeletionContext, User
class DeletedMessagesWithoutActorTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.author = User.objects.create(
id=55,
name='Robbie Rotten',
discriminator=55,
- avatar_hash=None
)
cls.data = {
@@ -26,14 +25,16 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase):
'id': 55,
'channel_id': 5555,
'content': "Terror Billy is a meanie",
- 'embeds': []
+ 'embeds': [],
+ 'attachments': []
},
{
'author': cls.author.id,
'id': 56,
'channel_id': 5555,
'content': "If you purge this, you're evil",
- 'embeds': []
+ 'embeds': [],
+ 'attachments': []
}
]
}
@@ -48,12 +49,11 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase):
class DeletedMessagesWithActorTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.author = cls.actor = User.objects.create(
id=12904,
name='Joe Armstrong',
discriminator=1245,
- avatar_hash=None
)
cls.data = {
@@ -65,7 +65,8 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase):
'id': 12903,
'channel_id': 1824,
'content': "I hate trailing commas",
- 'embeds': []
+ 'embeds': [],
+ 'attachments': []
},
]
}
diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py
index f6c78391..e560a2fd 100644
--- a/pydis_site/apps/api/tests/test_documentation_links.py
+++ b/pydis_site/apps/api/tests/test_documentation_links.py
@@ -57,7 +57,7 @@ class EmptyDatabaseDocumentationLinkAPITests(APISubdomainTestCase):
class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.doc_link = DocumentationLink.objects.create(
package='testpackage',
base_url='https://example.com',
@@ -141,7 +141,7 @@ class DocumentationLinkCreationTests(APISubdomainTestCase):
class DocumentationLinkDeletionTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.doc_link = DocumentationLink.objects.create(
package='example',
base_url='https://example.com',
diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py
new file mode 100644
index 00000000..188c0fff
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_filterlists.py
@@ -0,0 +1,122 @@
+from django_hosts.resolvers import reverse
+
+from pydis_site.apps.api.models import FilterList
+from pydis_site.apps.api.tests.base import APISubdomainTestCase
+
+URL = reverse('bot:filterlist-list', host='api')
+JPEG_ALLOWLIST = {
+ "type": 'FILE_FORMAT',
+ "allowed": True,
+ "content": ".jpeg",
+}
+PNG_ALLOWLIST = {
+ "type": 'FILE_FORMAT',
+ "allowed": True,
+ "content": ".png",
+}
+
+
+class UnauthenticatedTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_cannot_read_allowedlist_list(self):
+ response = self.client.get(URL)
+
+ self.assertEqual(response.status_code, 401)
+
+
+class EmptyDatabaseTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ FilterList.objects.all().delete()
+
+ def test_returns_empty_object(self):
+ response = self.client.get(URL)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+
+class FetchTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ FilterList.objects.all().delete()
+ cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST)
+ cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST)
+
+ def test_returns_name_in_list(self):
+ response = self.client.get(URL)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json()[0]["content"], self.jpeg_format.content)
+ self.assertEqual(response.json()[1]["content"], self.png_format.content)
+
+ def test_returns_single_item_by_id(self):
+ response = self.client.get(f'{URL}/{self.jpeg_format.id}')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json().get("content"), self.jpeg_format.content)
+
+ def test_returns_filter_list_types(self):
+ response = self.client.get(f'{URL}/get-types')
+
+ self.assertEqual(response.status_code, 200)
+ for api_type, model_type in zip(response.json(), FilterList.FilterListType.choices):
+ self.assertEquals(api_type[0], model_type[0])
+ self.assertEquals(api_type[1], model_type[1])
+
+
+class CreationTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ FilterList.objects.all().delete()
+
+ def test_returns_400_for_missing_params(self):
+ no_type_json = {
+ "allowed": True,
+ "content": ".jpeg"
+ }
+ no_allowed_json = {
+ "type": "FILE_FORMAT",
+ "content": ".jpeg"
+ }
+ no_content_json = {
+ "allowed": True,
+ "type": "FILE_FORMAT"
+ }
+ cases = [{}, no_type_json, no_allowed_json, no_content_json]
+
+ for case in cases:
+ with self.subTest(case=case):
+ response = self.client.post(URL, data=case)
+ self.assertEqual(response.status_code, 400)
+
+ def test_returns_201_for_successful_creation(self):
+ response = self.client.post(URL, data=JPEG_ALLOWLIST)
+ self.assertEqual(response.status_code, 201)
+
+ def test_returns_400_for_duplicate_creation(self):
+ self.client.post(URL, data=JPEG_ALLOWLIST)
+ response = self.client.post(URL, data=JPEG_ALLOWLIST)
+ self.assertEqual(response.status_code, 400)
+
+
+class DeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ FilterList.objects.all().delete()
+ cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST)
+ cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST)
+
+ def test_deleting_unknown_id_returns_404(self):
+ response = self.client.delete(f"{URL}/200")
+ self.assertEqual(response.status_code, 404)
+
+ def test_deleting_known_id_returns_204(self):
+ response = self.client.delete(f"{URL}/{self.jpeg_format.id}")
+ self.assertEqual(response.status_code, 204)
+
+ response = self.client.get(f"{URL}/{self.jpeg_format.id}")
+ self.assertNotIn(self.png_format.content, response.json())
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index c58c32e2..93ef8171 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -1,10 +1,13 @@
from datetime import datetime as dt, timedelta, timezone
+from unittest.mock import patch
from urllib.parse import quote
+from django.db.utils import IntegrityError
from django_hosts.resolvers import reverse
from .base import APISubdomainTestCase
from ..models import Infraction, User
+from ..serializers import InfractionSerializer
class UnauthenticatedTests(APISubdomainTestCase):
@@ -39,12 +42,11 @@ class UnauthenticatedTests(APISubdomainTestCase):
class InfractionTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.user = User.objects.create(
id=5,
name='james',
discriminator=1,
- avatar_hash=None
)
cls.ban_hidden = Infraction.objects.create(
user_id=cls.user.id,
@@ -52,7 +54,8 @@ class InfractionTests(APISubdomainTestCase):
type='ban',
reason='He terk my jerb!',
hidden=True,
- expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
+ expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc),
+ active=True
)
cls.ban_inactive = Infraction.objects.create(
user_id=cls.user.id,
@@ -160,12 +163,16 @@ class InfractionTests(APISubdomainTestCase):
class CreationTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.user = User.objects.create(
id=5,
name='james',
discriminator=1,
- avatar_hash=None
+ )
+ cls.second_user = User.objects.create(
+ id=6,
+ name='carl',
+ discriminator=2,
)
def test_accepts_valid_data(self):
@@ -176,7 +183,8 @@ class CreationTests(APISubdomainTestCase):
'type': 'ban',
'reason': 'He terk my jerb!',
'hidden': True,
- 'expires_at': '5018-11-20T15:52:00+00:00'
+ 'expires_at': '5018-11-20T15:52:00+00:00',
+ 'active': True,
}
response = self.client.post(url, data=data)
@@ -200,7 +208,8 @@ class CreationTests(APISubdomainTestCase):
url = reverse('bot:infraction-list', host='api')
data = {
'actor': self.user.id,
- 'type': 'kick'
+ 'type': 'kick',
+ 'active': False,
}
response = self.client.post(url, data=data)
@@ -214,7 +223,8 @@ class CreationTests(APISubdomainTestCase):
data = {
'user': 1337,
'actor': self.user.id,
- 'type': 'kick'
+ 'type': 'kick',
+ 'active': True,
}
response = self.client.post(url, data=data)
@@ -228,7 +238,8 @@ class CreationTests(APISubdomainTestCase):
data = {
'user': self.user.id,
'actor': self.user.id,
- 'type': 'hug'
+ 'type': 'hug',
+ 'active': True,
}
response = self.client.post(url, data=data)
@@ -243,7 +254,8 @@ class CreationTests(APISubdomainTestCase):
'user': self.user.id,
'actor': self.user.id,
'type': 'ban',
- 'expires_at': '20/11/5018 15:52:00'
+ 'expires_at': '20/11/5018 15:52:00',
+ 'active': True,
}
response = self.client.post(url, data=data)
@@ -263,7 +275,8 @@ class CreationTests(APISubdomainTestCase):
'user': self.user.id,
'actor': self.user.id,
'type': infraction_type,
- 'expires_at': '5018-11-20T15:52:00+00:00'
+ 'expires_at': '5018-11-20T15:52:00+00:00',
+ 'active': False,
}
response = self.client.post(url, data=data)
@@ -280,7 +293,8 @@ class CreationTests(APISubdomainTestCase):
'user': self.user.id,
'actor': self.user.id,
'type': infraction_type,
- 'hidden': True
+ 'hidden': True,
+ 'active': False,
}
response = self.client.post(url, data=data)
@@ -297,6 +311,7 @@ class CreationTests(APISubdomainTestCase):
'actor': self.user.id,
'type': 'note',
'hidden': False,
+ 'active': False,
}
response = self.client.post(url, data=data)
@@ -305,31 +320,223 @@ class CreationTests(APISubdomainTestCase):
'hidden': [f'{data["type"]} infractions must be hidden.']
})
+ 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')
+ restricted_types = (
+ ('note', True),
+ ('warning', False),
+ ('kick', False),
+ )
+
+ for infraction_type, hidden in restricted_types:
+ with self.subTest(infraction_type=infraction_type):
+ invalid_infraction = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': infraction_type,
+ 'reason': 'Take me on!',
+ 'hidden': hidden,
+ 'active': True,
+ 'expires_at': None,
+ }
+ response = self.client.post(url, data=invalid_infraction)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(
+ response.json(),
+ {'active': [f'{infraction_type} infractions cannot be active.']}
+ )
+
+ 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')
+ active_infraction_types = ('mute', 'ban', 'superstar')
+
+ for infraction_type in active_infraction_types:
+ with self.subTest(infraction_type=infraction_type):
+ first_active_infraction = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': infraction_type,
+ 'reason': 'Take me on!',
+ 'active': True,
+ 'expires_at': '2019-10-04T12:52:00+00:00'
+ }
+
+ # Post the first active infraction of a type and confirm it's accepted.
+ first_response = self.client.post(url, data=first_active_infraction)
+ self.assertEqual(first_response.status_code, 201)
+
+ second_active_infraction = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': infraction_type,
+ 'reason': 'Take on me!',
+ 'active': True,
+ 'expires_at': '2019-10-04T12:52:00+00:00'
+ }
+ second_response = self.client.post(url, data=second_active_infraction)
+ self.assertEqual(second_response.status_code, 400)
+ self.assertEqual(
+ second_response.json(),
+ {
+ 'non_field_errors': [
+ 'This user already has an active infraction of this type.'
+ ]
+ }
+ )
+
+ 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')
+ first_active_infraction = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'mute',
+ 'reason': 'Be silent!',
+ 'hidden': True,
+ 'active': True,
+ 'expires_at': '2019-10-04T12:52:00+00:00'
+ }
+ second_active_infraction = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'ban',
+ 'reason': 'Be gone!',
+ 'hidden': True,
+ 'active': True,
+ 'expires_at': '2019-10-05T12:52:00+00:00'
+ }
+ # Post the first active infraction of a type and confirm it's accepted.
+ first_response = self.client.post(url, data=first_active_infraction)
+ self.assertEqual(first_response.status_code, 201)
+
+ # Post the first active infraction of a type and confirm it's accepted.
+ second_response = self.client.post(url, data=second_active_infraction)
+ self.assertEqual(second_response.status_code, 201)
+
+ def test_unique_constraint_raises_integrity_error_on_second_active_of_same_type(self):
+ """Do we raise `IntegrityError` for the second active infraction of a type for a user?"""
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=True,
+ reason="The first active ban"
+ )
+ with self.assertRaises(IntegrityError):
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=True,
+ reason="The second active ban"
+ )
+
+ def test_unique_constraint_accepts_active_infraction_after_inactive_infraction(self):
+ """Do we accept an active infraction if the others of the same type are inactive?"""
+ try:
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=False,
+ reason="The first inactive ban"
+ )
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=False,
+ reason="The second inactive ban"
+ )
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=True,
+ reason="The first active ban"
+ )
+ except IntegrityError:
+ self.fail("An unexpected IntegrityError was raised.")
+
+ @patch(f"{__name__}.Infraction")
+ def test_if_accepts_active_infraction_test_catches_integrity_error(self, infraction_patch):
+ """Does the test properly catch the IntegrityError and raise an AssertionError?"""
+ infraction_patch.objects.create.side_effect = IntegrityError
+ with self.assertRaises(AssertionError, msg="An unexpected IntegrityError was raised."):
+ self.test_unique_constraint_accepts_active_infraction_after_inactive_infraction()
+
+ def test_unique_constraint_accepts_second_active_of_different_type(self):
+ """Do we accept a second active infraction of a different type for a given user?"""
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=True,
+ reason="The first active ban"
+ )
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="mute",
+ active=True,
+ reason="The first active mute"
+ )
+
+ def test_unique_constraint_accepts_active_infractions_for_different_users(self):
+ """Do we accept two active infractions of the same type for two different users?"""
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type="ban",
+ active=True,
+ reason="An active ban for the first user"
+ )
+ Infraction.objects.create(
+ user=self.second_user,
+ actor=self.second_user,
+ type="ban",
+ active=False,
+ reason="An active ban for the second user"
+ )
+
+ def test_integrity_error_if_missing_active_field(self):
+ pattern = 'null value in column "active" violates not-null constraint'
+ with self.assertRaisesRegex(IntegrityError, pattern):
+ Infraction.objects.create(
+ user=self.user,
+ actor=self.user,
+ type='ban',
+ reason='A reason.',
+ )
+
class ExpandedTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.user = User.objects.create(
id=5,
name='james',
discriminator=1,
- avatar_hash=None
)
cls.kick = Infraction.objects.create(
user_id=cls.user.id,
actor_id=cls.user.id,
- type='kick'
+ type='kick',
+ active=False
)
cls.warning = Infraction.objects.create(
user_id=cls.user.id,
actor_id=cls.user.id,
- type='warning'
+ type='warning',
+ active=False,
)
def check_expanded_fields(self, infraction):
for key in ('user', 'actor'):
obj = infraction[key]
- for field in ('id', 'name', 'discriminator', 'avatar_hash', 'roles', 'in_guild'):
+ for field in ('id', 'name', 'discriminator', 'roles', 'in_guild'):
self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}')
def test_list_expanded(self):
@@ -349,7 +556,8 @@ class ExpandedTests(APISubdomainTestCase):
data = {
'user': self.user.id,
'actor': self.user.id,
- 'type': 'warning'
+ 'type': 'warning',
+ 'active': False
}
response = self.client.post(url, data=data)
@@ -378,3 +586,80 @@ class ExpandedTests(APISubdomainTestCase):
infraction = Infraction.objects.get(id=self.kick.id)
self.assertEqual(infraction.active, data['active'])
self.check_expanded_fields(response.json())
+
+
+class SerializerTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = User.objects.create(
+ id=5,
+ name='james',
+ discriminator=1,
+ )
+
+ def create_infraction(self, _type: str, active: bool):
+ return Infraction.objects.create(
+ user_id=self.user.id,
+ actor_id=self.user.id,
+ type=_type,
+ reason='A reason.',
+ expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc),
+ active=active
+ )
+
+ def test_is_valid_if_active_infraction_with_same_fields_exists(self):
+ self.create_infraction('ban', active=True)
+ instance = self.create_infraction('ban', active=False)
+
+ data = {'reason': 'hello'}
+ serializer = InfractionSerializer(instance, data=data, partial=True)
+
+ self.assertTrue(serializer.is_valid(), msg=serializer.errors)
+
+ def test_validation_error_if_active_duplicate(self):
+ self.create_infraction('ban', active=True)
+ instance = self.create_infraction('ban', active=False)
+
+ data = {'active': True}
+ serializer = InfractionSerializer(instance, data=data, partial=True)
+
+ if not serializer.is_valid():
+ self.assertIn('non_field_errors', serializer.errors)
+
+ code = serializer.errors['non_field_errors'][0].code
+ msg = f'Expected failure on unique validator but got {serializer.errors}'
+ self.assertEqual(code, 'unique', msg=msg)
+ else: # pragma: no cover
+ self.fail('Validation unexpectedly succeeded.')
+
+ def test_is_valid_for_new_active_infraction(self):
+ self.create_infraction('ban', active=False)
+
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'ban',
+ 'reason': 'A reason.',
+ 'active': True
+ }
+ serializer = InfractionSerializer(data=data)
+
+ self.assertTrue(serializer.is_valid(), msg=serializer.errors)
+
+ def test_validation_error_if_missing_active_field(self):
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'ban',
+ 'reason': 'A reason.',
+ }
+ serializer = InfractionSerializer(data=data)
+
+ if not serializer.is_valid():
+ self.assertIn('active', serializer.errors)
+
+ code = serializer.errors['active'][0].code
+ msg = f'Expected failure on required active field but got {serializer.errors}'
+ self.assertEqual(code, 'required', msg=msg)
+ else: # pragma: no cover
+ self.fail('Validation unexpectedly succeeded.')
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index bce76942..853e6621 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -3,20 +3,20 @@ from datetime import datetime as dt
from django.test import SimpleTestCase
from django.utils import timezone
-from ..models import (
+from pydis_site.apps.api.models import (
DeletedMessage,
DocumentationLink,
Infraction,
Message,
MessageDeletionContext,
- ModelReprMixin,
Nomination,
OffTopicChannelName,
+ OffensiveMessage,
Reminder,
Role,
- Tag,
User
)
+from pydis_site.apps.api.models.mixins import ModelReprMixin
class SimpleClass(ModelReprMixin):
@@ -38,12 +38,14 @@ class StringDunderMethodTests(SimpleTestCase):
self.nomination = Nomination(
id=123,
actor=User(
- id=9876, name='Mr. Hemlock',
- discriminator=6666, avatar_hash=None
+ id=9876,
+ name='Mr. Hemlock',
+ discriminator=6666,
),
user=User(
- id=9876, name="Hemlock's Cat",
- discriminator=7777, avatar_hash=None
+ id=9876,
+ name="Hemlock's Cat",
+ discriminator=7777,
),
reason="He purrrrs like the best!",
)
@@ -52,15 +54,17 @@ class StringDunderMethodTests(SimpleTestCase):
DeletedMessage(
id=45,
author=User(
- id=444, name='bill',
- discriminator=5, avatar_hash=None
+ id=444,
+ name='bill',
+ discriminator=5,
),
channel_id=666,
content="wooey",
deletion_context=MessageDeletionContext(
actor=User(
- id=5555, name='shawn',
- discriminator=555, avatar_hash=None
+ id=5555,
+ name='shawn',
+ discriminator=555,
),
creation=dt.utcnow()
),
@@ -69,6 +73,11 @@ class StringDunderMethodTests(SimpleTestCase):
DocumentationLink(
'test', 'http://example.com', 'http://example.com'
),
+ OffensiveMessage(
+ id=602951077675139072,
+ channel_id=291284109232308226,
+ delete_date=dt(3000, 1, 1)
+ ),
OffTopicChannelName(name='bob-the-builders-playground'),
Role(
id=5, name='test role',
@@ -78,8 +87,9 @@ class StringDunderMethodTests(SimpleTestCase):
Message(
id=45,
author=User(
- id=444, name='bill',
- discriminator=5, avatar_hash=None
+ id=444,
+ name='bill',
+ discriminator=5,
),
channel_id=666,
content="wooey",
@@ -87,34 +97,42 @@ class StringDunderMethodTests(SimpleTestCase):
),
MessageDeletionContext(
actor=User(
- id=5555, name='shawn',
- discriminator=555, avatar_hash=None
+ id=5555,
+ name='shawn',
+ discriminator=555,
),
creation=dt.utcnow()
),
- Tag(
- title='bob',
- embed={'content': "the builder"}
- ),
User(
- id=5, name='bob',
- discriminator=1, avatar_hash=None
+ id=5,
+ name='bob',
+ discriminator=1,
),
Infraction(
- user_id=5, actor_id=5,
- type='kick', reason='He terk my jerb!'
+ user_id=5,
+ actor_id=5,
+ type='kick',
+ reason='He terk my jerb!'
),
Infraction(
- user_id=5, actor_id=5, hidden=True,
- type='kick', reason='He terk my jerb!',
+ user_id=5,
+ actor_id=5,
+ hidden=True,
+ type='kick',
+ reason='He terk my jerb!',
expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
),
Reminder(
author=User(
- id=452, name='billy',
- discriminator=5, avatar_hash=None
+ id=452,
+ name='billy',
+ discriminator=5,
),
channel_id=555,
+ jump_url=(
+ 'https://discordapp.com/channels/'
+ '267624335836053506/291284109232308226/463087129459949587'
+ ),
content="oh no",
expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
)
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index add5a7e4..b37135f8 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -8,12 +8,11 @@ from ..models import Nomination, User
class CreationTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.user = User.objects.create(
id=1234,
name='joe dart',
discriminator=1111,
- avatar_hash=None
)
def test_accepts_valid_data(self):
@@ -81,7 +80,7 @@ class CreationTests(APISubdomainTestCase):
'actor': ['This field is required.']
})
- def test_returns_400_for_missing_reason(self):
+ def test_returns_201_for_missing_reason(self):
url = reverse('bot:nomination-list', host='api')
data = {
'user': self.user.id,
@@ -89,10 +88,7 @@ class CreationTests(APISubdomainTestCase):
}
response = self.client.post(url, data=data)
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.json(), {
- 'reason': ['This field is required.']
- })
+ self.assertEqual(response.status_code, 201)
def test_returns_400_for_bad_user(self):
url = reverse('bot:nomination-list', host='api')
@@ -185,12 +181,11 @@ class CreationTests(APISubdomainTestCase):
class NominationTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.user = User.objects.create(
id=1234,
name='joe dart',
discriminator=1111,
- avatar_hash=None
)
cls.active_nomination = Nomination.objects.create(
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 9ab71409..3ab8b22d 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
@@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase):
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')
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')
response = self.client.get(f'{url}?random_items=no')
@@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
+ """Return empty list when no names in database."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
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')
response = self.client.get(f'{url}?random_items=5')
@@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
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')
response = self.client.get(f'{url}?random_items=totally-a-valid-integer')
@@ -47,6 +52,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')
response = self.client.get(f'{url}?random_items=-5')
@@ -58,11 +64,12 @@ class EmptyDatabaseTests(APISubdomainTestCase):
class ListTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
- cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
- cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
+ 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)
def test_returns_name_in_list(self):
+ """Return all off-topic channel names."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -76,11 +83,21 @@ class ListTests(APISubdomainTestCase):
)
def test_returns_single_item_with_random_items_param_set_to_1(self):
+ """Return not-used name instead used."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=1')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
+ self.assertEqual(response.json(), [self.test_name.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')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name])
class CreationTests(APISubdomainTestCase):
@@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase):
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')
names = (
'𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹',
@@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_400_for_missing_name_param(self):
+ """Return error message when name not provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.post(url)
self.assertEqual(response.status_code, 400)
@@ -112,6 +131,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')
invalid_names = (
'space between words',
@@ -129,23 +149,26 @@ class CreationTests(APISubdomainTestCase):
class DeletionTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ 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')
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')
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')
response = self.client.delete(url)
diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py
new file mode 100644
index 00000000..0f3dbffa
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_offensive_message.py
@@ -0,0 +1,155 @@
+import datetime
+
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import OffensiveMessage
+
+
+class CreationTests(APISubdomainTestCase):
+ def test_accept_valid_data(self):
+ url = reverse('bot:offensivemessage-list', host='api')
+ delete_at = datetime.datetime.now() + datetime.timedelta(days=1)
+ data = {
+ 'id': '602951077675139072',
+ 'channel_id': '291284109232308226',
+ 'delete_date': delete_at.isoformat()[:-1]
+ }
+
+ aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc)
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+
+ offensive_message = OffensiveMessage.objects.get(id=response.json()['id'])
+ self.assertAlmostEqual(
+ aware_delete_at,
+ offensive_message.delete_date,
+ delta=datetime.timedelta(seconds=1)
+ )
+ self.assertEqual(data['id'], str(offensive_message.id))
+ 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')
+ delete_at = datetime.datetime.now() - datetime.timedelta(days=1)
+ data = {
+ 'id': '602951077675139072',
+ 'channel_id': '291284109232308226',
+ 'delete_date': delete_at.isoformat()[:-1]
+ }
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'delete_date': ['Date must be a future date']
+ })
+
+ def test_returns_400_on_negative_id_or_channel_id(self):
+ url = reverse('bot:offensivemessage-list', host='api')
+ delete_at = datetime.datetime.now() + datetime.timedelta(days=1)
+ data = {
+ 'id': '602951077675139072',
+ 'channel_id': '291284109232308226',
+ 'delete_date': delete_at.isoformat()[:-1]
+ }
+ cases = (
+ ('id', '-602951077675139072'),
+ ('channel_id', '-291284109232308226')
+ )
+
+ for field, invalid_value in cases:
+ with self.subTest(fied=field, invalid_value=invalid_value):
+ test_data = data.copy()
+ test_data.update({field: invalid_value})
+
+ response = self.client.post(url, test_data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ field: ['Ensure this value is greater than or equal to 0.']
+ })
+
+
+class ListTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ delete_at = datetime.datetime.now() + datetime.timedelta(days=1)
+ aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc)
+
+ cls.messages = [
+ {
+ 'id': 602951077675139072,
+ 'channel_id': 91284109232308226,
+ },
+ {
+ 'id': 645298201494159401,
+ 'channel_id': 592000283102674944
+ }
+ ]
+
+ cls.of1 = OffensiveMessage.objects.create(
+ **cls.messages[0],
+ delete_date=aware_delete_at.isoformat()
+ )
+ cls.of2 = OffensiveMessage.objects.create(
+ **cls.messages[1],
+ delete_date=aware_delete_at.isoformat()
+ )
+
+ # Expected API answer :
+ cls.messages[0]['delete_date'] = delete_at.isoformat() + 'Z'
+ cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z'
+
+ def test_get_data(self):
+ url = reverse('bot:offensivemessage-list', host='api')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ self.assertEqual(response.json(), self.messages)
+
+
+class DeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1)
+
+ cls.valid_offensive_message = OffensiveMessage.objects.create(
+ id=602951077675139072,
+ channel_id=291284109232308226,
+ delete_date=delete_at.isoformat()
+ )
+
+ def test_delete_data(self):
+ url = reverse(
+ 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,)
+ )
+
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code, 204)
+
+ self.assertFalse(
+ OffensiveMessage.objects.filter(id=self.valid_offensive_message.id).exists()
+ )
+
+
+class NotAllowedMethodsTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1)
+
+ cls.valid_offensive_message = OffensiveMessage.objects.create(
+ id=602951077675139072,
+ channel_id=291284109232308226,
+ delete_date=delete_at.isoformat()
+ )
+
+ def test_returns_405_for_patch_and_put_requests(self):
+ url = reverse(
+ 'bot:offensivemessage-detail', host='api', args=(self.valid_offensive_message.id,)
+ )
+ not_allowed_methods = (self.client.patch, self.client.put)
+
+ for method in not_allowed_methods:
+ with self.subTest(method=method):
+ response = method(url, {})
+ self.assertEqual(response.status_code, 405)
diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py
new file mode 100644
index 00000000..9dffb668
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_reminders.py
@@ -0,0 +1,221 @@
+from datetime import datetime
+
+from django.forms.models import model_to_dict
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import Reminder, User
+
+
+class UnauthedReminderAPITests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_list_returns_401(self):
+ url = reverse('bot:reminder-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_create_returns_401(self):
+ url = reverse('bot:reminder-list', host='api')
+ 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')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 401)
+
+
+class EmptyDatabaseReminderAPITests(APISubdomainTestCase):
+ def test_list_all_returns_empty_list(self):
+ url = reverse('bot:reminder-list', host='api')
+ 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')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+
+class ReminderCreationTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=1234,
+ name='Mermaid Man',
+ discriminator=1234,
+ )
+
+ def test_accepts_valid_data(self):
+ data = {
+ 'author': self.author.id,
+ 'content': 'Remember to...wait what was it again?',
+ 'expiration': datetime.utcnow().isoformat(),
+ 'jump_url': "https://www.google.com",
+ 'channel_id': 123,
+ 'mentions': [8888, 9999],
+ }
+ url = reverse('bot:reminder-list', host='api')
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+ self.assertIsNotNone(Reminder.objects.filter(id=1).first())
+
+ def test_rejects_invalid_data(self):
+ data = {
+ 'author': self.author.id, # Missing multiple required fields
+ }
+ url = reverse('bot:reminder-list', host='api')
+ 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):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=6789,
+ name='Barnacle Boy',
+ discriminator=6789,
+ )
+
+ cls.reminder = Reminder.objects.create(
+ author=cls.author,
+ content="Don't forget to set yourself a reminder",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="https://www.decliningmentalfaculties.com",
+ channel_id=123
+ )
+
+ def test_delete_unknown_reminder_returns_404(self):
+ url = reverse('bot:reminder-detail', args=('something',), host='api')
+ 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')
+ 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):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=6789,
+ name='Patrick Star',
+ discriminator=6789,
+ )
+
+ cls.reminder_one = Reminder.objects.create(
+ author=cls.author,
+ content="We should take Bikini Bottom, and push it somewhere else!",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="https://www.icantseemyforehead.com",
+ channel_id=123
+ )
+
+ cls.reminder_two = Reminder.objects.create(
+ author=cls.author,
+ content="Gahhh-I love being purple!",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="https://www.goofygoobersicecreampartyboat.com",
+ channel_id=123,
+ active=False
+ )
+
+ cls.rem_dict_one = model_to_dict(cls.reminder_one)
+ cls.rem_dict_one['expiration'] += 'Z' # Massaging a quirk of the response time format
+ cls.rem_dict_two = model_to_dict(cls.reminder_two)
+ 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')
+ 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')
+ 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')
+ 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):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=6789,
+ name='Reminder author',
+ discriminator=6789,
+ )
+
+ cls.reminder = Reminder.objects.create(
+ author=cls.author,
+ content="Reminder content",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="http://example.com/",
+ channel_id=123
+ )
+
+ def test_retrieve_unknown_returns_404(self):
+ url = reverse('bot:reminder-detail', args=("not_an_id",), host='api')
+ 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')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+
+class ReminderUpdateTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=666,
+ name='Man Ray',
+ discriminator=666,
+ )
+
+ cls.reminder = Reminder.objects.create(
+ author=cls.author,
+ content="Squash those do-gooders",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="https://www.decliningmentalfaculties.com",
+ channel_id=123
+ )
+
+ cls.data = {'content': 'Oops I forgot'}
+
+ def test_patch_updates_record(self):
+ url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api')
+ response = self.client.patch(url, data=self.data)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ Reminder.objects.filter(id=self.reminder.id).first().content,
+ self.data['content']
+ )
diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py
index 0a6cea9e..4d1a430c 100644
--- a/pydis_site/apps/api/tests/test_roles.py
+++ b/pydis_site/apps/api/tests/test_roles.py
@@ -6,7 +6,7 @@ from ..models import Role
class CreationTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.admins_role = Role.objects.create(
id=1,
name="Admins",
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index bbdd3ff4..4c0f6e27 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -36,7 +36,7 @@ class UnauthedUserAPITests(APISubdomainTestCase):
class CreationTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.role = Role.objects.create(
id=5,
name="Test role pls ignore",
@@ -49,7 +49,6 @@ class CreationTests(APISubdomainTestCase):
url = reverse('bot:user-list', host='api')
data = {
'id': 42,
- 'avatar_hash': "validavatarhashiswear",
'name': "Test",
'discriminator': 42,
'roles': [
@@ -63,7 +62,6 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.json(), data)
user = User.objects.get(id=42)
- self.assertEqual(user.avatar_hash, data['avatar_hash'])
self.assertEqual(user.name, data['name'])
self.assertEqual(user.discriminator, data['discriminator'])
self.assertEqual(user.in_guild, data['in_guild'])
@@ -73,7 +71,6 @@ class CreationTests(APISubdomainTestCase):
data = [
{
'id': 5,
- 'avatar_hash': "hahayes",
'name': "test man",
'discriminator': 42,
'roles': [
@@ -83,7 +80,6 @@ class CreationTests(APISubdomainTestCase):
},
{
'id': 8,
- 'avatar_hash': "maybenot",
'name': "another test man",
'discriminator': 555,
'roles': [],
@@ -99,7 +95,6 @@ class CreationTests(APISubdomainTestCase):
url = reverse('bot:user-list', host='api')
data = {
'id': 5,
- 'avatar_hash': "hahayes",
'name': "test man",
'discriminator': 42,
'roles': [
@@ -114,7 +109,6 @@ class CreationTests(APISubdomainTestCase):
url = reverse('bot:user-list', host='api')
data = {
'id': True,
- 'avatar_hash': 1902831,
'discriminator': "totally!"
}
@@ -124,7 +118,7 @@ class CreationTests(APISubdomainTestCase):
class UserModelTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.role_top = Role.objects.create(
id=777,
name="High test role",
@@ -148,16 +142,14 @@ class UserModelTests(APISubdomainTestCase):
)
cls.user_with_roles = User.objects.create(
id=1,
- avatar_hash="coolavatarhash",
name="Test User with two roles",
discriminator=1111,
in_guild=True,
)
- cls.user_with_roles.roles.add(cls.role_bottom, cls.role_top)
+ cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id])
cls.user_without_roles = User.objects.create(
id=2,
- avatar_hash="coolavatarhash",
name="Test User without roles",
discriminator=2222,
in_guild=True,
diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py
index 4222f0c0..8bb7b917 100644
--- a/pydis_site/apps/api/tests/test_validators.py
+++ b/pydis_site/apps/api/tests/test_validators.py
@@ -1,8 +1,11 @@
+from datetime import datetime, timezone
+
from django.core.exceptions import ValidationError
from django.test import TestCase
from ..models.bot.bot_setting import validate_bot_setting_name
-from ..models.bot.tag import validate_tag_embed
+from ..models.bot.offensive_message import future_date_validator
+from ..models.utils import validate_embed
REQUIRED_KEYS = (
@@ -22,77 +25,77 @@ class BotSettingValidatorTests(TestCase):
class TagEmbedValidatorTests(TestCase):
def test_rejects_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed('non-empty non-mapping')
+ validate_embed('non-empty non-mapping')
def test_rejects_missing_required_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'unknown': "key"
})
def test_rejects_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'provider': "??",
'title': ""
})
def test_rejects_empty_required_key(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': ''
})
def test_rejects_list_as_embed(self):
with self.assertRaises(ValidationError):
- validate_tag_embed([])
+ validate_embed([])
def test_rejects_required_keys_and_unknown_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "the duck walked up to the lemonade stand",
'and': "he said to the man running the stand"
})
def test_rejects_too_long_title(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': 'a' * 257
})
def test_rejects_too_many_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [{} for _ in range(26)]
})
def test_rejects_too_long_description(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'description': 'd' * 2049
})
def test_allows_valid_embed(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'description': "look at my embed, my embed is amazing"
})
def test_allows_unvalidated_fields(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'provider': "what am I??"
})
def test_rejects_fields_as_list_of_non_mappings(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': ['abc']
})
def test_rejects_fields_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'what': "is this field"
@@ -102,7 +105,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_fields_with_too_long_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "a" * 257
@@ -112,7 +115,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_one_correct_one_incorrect_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -128,7 +131,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_missing_required_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -139,7 +142,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_invalid_inline_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -150,7 +153,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_valid_fields(self):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "valid",
@@ -171,14 +174,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': []
})
def test_rejects_footer_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'duck': "quack"
@@ -187,7 +190,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_with_empty_text(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': ""
@@ -195,7 +198,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_footer_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': "django good"
@@ -204,14 +207,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': []
})
def test_rejects_author_with_unknown_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'field': "that is unknown"
@@ -220,7 +223,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_empty_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': ""
@@ -229,7 +232,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
# Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour
@@ -239,9 +242,18 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_author_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': "Bob"
}
})
+
+
+class OffensiveMessageValidatorsTests(TestCase):
+ def test_accepts_future_date(self):
+ future_date_validator(datetime(3000, 1, 1, tzinfo=timezone.utc))
+
+ def test_rejects_non_future_date(self):
+ with self.assertRaises(ValidationError):
+ future_date_validator(datetime(1000, 1, 1, tzinfo=timezone.utc))