Skip to content

Commit

Permalink
Fix conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
hedyhli committed Dec 14, 2023
2 parents ed5dfcf + 0da777e commit d7fc844
Show file tree
Hide file tree
Showing 21 changed files with 739 additions and 254 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# When updating the Python version here, please make sure to also
# update the contributor guide, which can be found at
# pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
# Thank you!
FROM ghcr.io/owl-corp/python-poetry-base:3.11-slim

# Allow service to handle stops gracefully
Expand Down
362 changes: 174 additions & 188 deletions poetry.lock

Large diffs are not rendered by default.

34 changes: 31 additions & 3 deletions pydis_site/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
from typing import Any

from django.db import models
from django.db.models.query import QuerySet
from django.db.utils import IntegrityError
from rest_framework.exceptions import NotFound
Expand Down Expand Up @@ -35,6 +36,30 @@
User
)

class FrozenFieldsMixin:
"""
Serializer mixin that allows adding non-updateable fields to a serializer.
To use, inherit from the mixin and specify the fields that should only be
written to on creation in the `frozen_fields` attribute of the `Meta` class
in a serializer.
See also the DRF discussion for this feature at
https://github.com/encode/django-rest-framework/discussions/8606, which may
eventually provide an official way to implement this.
"""

def update(self, instance: models.Model, validated_data: dict) -> models.Model:
"""Validate that no frozen fields were changed and update the instance."""
for field_name in getattr(self.Meta, 'frozen_fields', ()):
if field_name in validated_data:
raise ValidationError(
{
field_name: ["This field cannot be updated."]
}
)
return super().update(instance, validated_data)


class BotSettingSerializer(ModelSerializer):
"""A class providing (de-)serialization of `BotSetting` instances."""
Expand Down Expand Up @@ -426,7 +451,7 @@ def to_representation(self, instance: FilterList) -> dict:
# endregion


class InfractionSerializer(ModelSerializer):
class InfractionSerializer(FrozenFieldsMixin, ModelSerializer):
"""A class providing (de-)serialization of `Infraction` instances."""

class Meta:
Expand All @@ -447,6 +472,7 @@ class Meta:
'dm_sent',
'jump_url'
)
frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden')

def validate(self, attrs: dict) -> dict:
"""Validate data constraints for the given data and abort if it is invalid."""
Expand Down Expand Up @@ -683,7 +709,7 @@ class Meta:
fields = ('nomination', 'actor', 'reason', 'inserted_at')


class NominationSerializer(ModelSerializer):
class NominationSerializer(FrozenFieldsMixin, ModelSerializer):
"""A class providing (de-)serialization of `Nomination` instances."""

entries = NominationEntrySerializer(many=True, read_only=True)
Expand All @@ -703,13 +729,15 @@ class Meta:
'entries',
'thread_id'
)
frozen_fields = ('id', 'inserted_at', 'user', 'ended_at')


class OffensiveMessageSerializer(ModelSerializer):
class OffensiveMessageSerializer(FrozenFieldsMixin, ModelSerializer):
"""A class providing (de-)serialization of `OffensiveMessage` instances."""

class Meta:
"""Metadata defined for the Django REST Framework."""

model = OffensiveMessage
fields = ('id', 'channel_id', 'delete_date')
frozen_fields = ('id', 'channel_id')
62 changes: 62 additions & 0 deletions pydis_site/apps/api/tests/test_github_webhook_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from unittest import mock
from urllib.error import HTTPError

from django.urls import reverse
from rest_framework.test import APITestCase

from pydis_site.apps.api.views import GitHubWebhookFilterView

class GitHubWebhookFilterAPITests(APITestCase):
def test_ignores_bot_sender(self):
url = reverse('api:github-webhook-filter', args=('id', 'token'))
payload = {'sender': {'login': 'limette', 'type': 'bot'}}
headers = {'X-GitHub-Event': 'pull_request_review'}
response = self.client.post(url, data=payload, headers=headers)
self.assertEqual(response.status_code, 203)

def test_accepts_interesting_events(self):
url = reverse('api:github-webhook-filter', args=('id', 'token'))
payload = {
'ref': 'refs/heads/master',
'pull_request': {
'user': {
'login': "lemon",
}
},
'review': {
'state': 'commented',
'body': "Amazing!!!"
},
'repository': {
'name': 'black',
'owner': {
'login': 'psf',
}
}
}
headers = {'X-GitHub-Event': 'pull_request_review'}

with mock.patch('urllib.request.urlopen') as urlopen:
urlopen.return_value = mock.MagicMock()
context_mock = urlopen.return_value.__enter__.return_value
context_mock.status = 299
context_mock.getheaders.return_value = [('X-Clacks-Overhead', 'Joe Armstrong')]
context_mock.read.return_value = b'{"status": "ok"}'

response = self.client.post(url, data=payload, headers=headers)
self.assertEqual(response.status_code, context_mock.status)
self.assertEqual(response.headers.get('X-Clacks-Overhead'), 'Joe Armstrong')

def test_rate_limit_is_logged_to_sentry(self):
url = reverse('api:github-webhook-filter', args=('id', 'token'))
payload = {}
headers = {'X-GitHub-Event': 'pull_request_review'}
with (
mock.patch('urllib.request.urlopen') as urlopen,
mock.patch.object(GitHubWebhookFilterView, "logger") as logger,
):
urlopen.side_effect = HTTPError(None, 429, 'Too Many Requests', {}, None)
logger.warning = mock.PropertyMock()
self.client.post(url, data=payload, headers=headers)

logger.warning.assert_called_once()
2 changes: 1 addition & 1 deletion pydis_site/apps/api/tests/test_nominations.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_returns_200_update_reason_on_active_with_actor(self):
def test_returns_400_on_frozen_field_update(self):
url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,))
data = {
'user': "Theo Katzman"
'user': 1234
}

response = self.client.patch(url, data=data)
Expand Down
76 changes: 55 additions & 21 deletions pydis_site/apps/api/tests/test_offensive_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
from pydis_site.apps.api.models import OffensiveMessage


def create_offensive_message() -> OffensiveMessage:
"""Creates and returns an `OffensiveMessgage` record for tests."""
delete_at = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=1)

return OffensiveMessage.objects.create(
id=602951077675139072,
channel_id=291284109232308226,
delete_date=delete_at,
)


class CreationTests(AuthenticatedAPITestCase):
def test_accept_valid_data(self):
url = reverse('api:bot:offensivemessage-list')
Expand Down Expand Up @@ -111,13 +122,7 @@ def test_get_data(self):
class DeletionTests(AuthenticatedAPITestCase):
@classmethod
def setUpTestData(cls):
delete_at = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=1)

cls.valid_offensive_message = OffensiveMessage.objects.create(
id=602951077675139072,
channel_id=291284109232308226,
delete_date=delete_at.isoformat()
)
cls.valid_offensive_message = create_offensive_message()

def test_delete_data(self):
url = reverse(
Expand All @@ -132,24 +137,53 @@ def test_delete_data(self):
)


class NotAllowedMethodsTests(AuthenticatedAPITestCase):
class UpdateOffensiveMessageTestCase(AuthenticatedAPITestCase):
@classmethod
def setUpTestData(cls):
delete_at = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=1)
cls.message = create_offensive_message()
cls.in_one_week = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(days=7)

def test_updating_message(self):
url = reverse('api:bot:offensivemessage-detail', args=(self.message.id,))
data = {'delete_date': self.in_one_week.isoformat()}
update_response = self.client.patch(url, data=data)
self.assertEqual(update_response.status_code, 200)

cls.valid_offensive_message = OffensiveMessage.objects.create(
id=602951077675139072,
channel_id=291284109232308226,
delete_date=delete_at.isoformat()
self.message.refresh_from_db()
self.assertAlmostEqual(
self.message.delete_date,
self.in_one_week,
delta=datetime.timedelta(seconds=1),
)

def test_returns_405_for_patch_and_put_requests(self):
url = reverse(
'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,)
def test_updating_write_once_fields(self):
"""Fields such as the channel ID may not be updated."""
url = reverse('api:bot:offensivemessage-detail', args=(self.message.id,))
data = {'channel_id': self.message.channel_id + 1}
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {'channel_id': ["This field cannot be updated."]})

def test_updating_nonexistent_message(self):
url = reverse('api:bot:offensivemessage-detail', args=(self.message.id + 1,))
data = {'delete_date': self.in_one_week}

response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 404)
self.message.refresh_from_db()
self.assertNotAlmostEqual(
self.message.delete_date,
self.in_one_week,
delta=datetime.timedelta(seconds=1),
)
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)

class NotAllowedMethodsTests(AuthenticatedAPITestCase):
@classmethod
def setUpTestData(cls):
cls.message = create_offensive_message()

def test_returns_405_for_get(self):
url = reverse('api:bot:offensivemessage-detail', args=(self.message.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 405)
12 changes: 11 additions & 1 deletion pydis_site/apps/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from .views import GitHubArtifactsView, HealthcheckView, RulesView
from .views import (
GitHubArtifactsView,
GitHubWebhookFilterView,
HealthcheckView,
RulesView,
)
from .viewsets import (
AocAccountLinkViewSet,
AocCompletionistBlockViewSet,
Expand Down Expand Up @@ -101,4 +106,9 @@
GitHubArtifactsView.as_view(),
name="github-artifacts"
),
path(
'github/webhook-filter/<str:webhook_id>/<str:webhook_token>',
GitHubWebhookFilterView.as_view(),
name='github-webhook-filter'
),
)
Loading

0 comments on commit d7fc844

Please sign in to comment.