Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webapp authentication #121

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/wagtail_live/webapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class Message(models.Model):
)
show = models.BooleanField(
default=True,
blank=True,
help_text="Indicates if this message is shown/hidden",
)
content = models.TextField(help_text="Content of the message")
Expand Down
2 changes: 1 addition & 1 deletion src/wagtail_live/webapp/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def get_channel_id_from_message(self, message):
def get_message_id_from_message(self, message):
"""See base class."""

return message["id"]
return str(message["id"])

def get_message_text(self, message):
"""See base class."""
Expand Down
1 change: 1 addition & 0 deletions src/wagtail_live/webapp/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ["id", "channel", "created", "modified", "show", "content", "images"]
extra_kwargs = {"show": {"default": True}}

def create(self, validated_data):
images = validated_data.pop("images")
Expand Down
42 changes: 34 additions & 8 deletions src/wagtail_live/webapp/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
""" Webapp views """

from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ImproperlyConfigured
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from django.views.generic import DetailView, ListView
from rest_framework import status
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -19,10 +24,25 @@
def send_update(update_type, data):
event = {"update_type": update_type}
event.update(data)
LIVE_RECEIVER.dispatch_event(event=event)
try:
LIVE_RECEIVER.dispatch_event(event=event)
except KeyError:
# Message has been deleted from elsewhere.
pass


class ChannelListView(ListView):
class WebappLoginRequiredMixin(LoginRequiredMixin):
def get_login_url(self):
login_url = getattr(settings, "WEBAPP_LOGIN_URL", "")
if not login_url:
raise ImproperlyConfigured(
"You haven't specified the WEBAPP_LOGIN_URL in your settings. "
"It is required if you intend to use the webapp interface."
)
return str(login_url)


class ChannelListView(WebappLoginRequiredMixin, ListView):
"""List all channels"""

model = Channel
Expand All @@ -32,7 +52,7 @@ class ChannelListView(ListView):
channels_list_view = ChannelListView.as_view()


class ChannelDetailView(DetailView):
class ChannelDetailView(WebappLoginRequiredMixin, DetailView):
"""Channel details view"""

model = Channel
Expand All @@ -44,7 +64,12 @@ class ChannelDetailView(DetailView):
channel_detail_view = ChannelDetailView.as_view()


class CreateChannelView(APIView):
class WebappAPIView(APIView):
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]


class CreateChannelView(WebappAPIView):
def post(self, request):
"""API endpoint: create a new channel"""

Expand All @@ -58,7 +83,7 @@ def post(self, request):
create_channel_view = CreateChannelView.as_view()


class DeleteChannelView(APIView):
class DeleteChannelView(WebappAPIView):
slug_field = "channel_name"
slug_url_kwarg = "channel_name"

Expand All @@ -73,12 +98,13 @@ def delete(self, request, channel_name):
delete_channel_view = DeleteChannelView.as_view()


class CreateMessageView(APIView):
class CreateMessageView(WebappAPIView):
renderer_classes = [TemplateHTMLRenderer]
template_name = "webapp/message.html"

def post(self, request):
"""API endpoint: create a new message"""

serializer = MessageSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
Expand All @@ -91,7 +117,7 @@ def post(self, request):
create_message_view = CreateMessageView.as_view()


class MessageDetailView(APIView):
class MessageDetailView(WebappAPIView):
"""
Retrieve, update or delete a Message instance.
"""
Expand Down Expand Up @@ -128,7 +154,7 @@ def delete(self, request, pk):
message_detail_view = MessageDetailView.as_view()


class DeleteImageView(APIView):
class DeleteImageView(WebappAPIView):
def delete(self, request, pk):
"""API endpoint: delete an image by its ID"""

Expand Down
6 changes: 3 additions & 3 deletions tests/wagtail_live/receivers/test_webappreceiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_get_channel_id_from_message(webapp_receiver, message):

def test_get_message_id_from_message(webapp_receiver, message):
got = webapp_receiver.get_message_id_from_message(message)
assert got == message["id"]
assert got == str(message["id"])


def test_get_message_text(webapp_receiver, message):
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_get_message_files_if_files(webapp_receiver, image, message):

def test_get_message_id_from_edited_message(webapp_receiver, edited_message):
got = webapp_receiver.get_message_id_from_edited_message(edited_message)
assert got == edited_message["id"]
assert got == str(edited_message["id"])


def test_get_message_text_from_edited_message(webapp_receiver, edited_message):
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_add_message(blog_page_factory, webapp_receiver, message):
assert len(page.live_posts) == 2

post_added = page.live_posts[0].value
assert post_added["message_id"] == message["id"]
assert post_added["message_id"] == str(message["id"])

message_parts = message["content"].split("\n")
content = post_added["content"]
Expand Down
38 changes: 38 additions & 0 deletions tests/wagtail_live/webapp/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase


class AuthenticationTests(TestCase):
def test_missing_webapp_login_url_setting(self):
expected = (
"You haven't specified the WEBAPP_LOGIN_URL in your settings. "
"It is required if you intend to use the webapp interface."
)
with self.assertRaisesMessage(ImproperlyConfigured, expected):
self.client.get("/webapp/channels/")

def test_login_required_for_channels_views(self):
webapp_login_url = "/login/"

with self.settings(WEBAPP_LOGIN_URL=webapp_login_url):
response = self.client.get("/webapp/channels/")
self.assertEqual(response.url, f"{webapp_login_url}?next=/webapp/channels/")

response = self.client.get("/webapp/channels/channel_2/")
self.assertEqual(
response.url, f"{webapp_login_url}?next=/webapp/channels/channel_2/"
)

def test_auth_required_for_api_views(self):
webapp_login_url = "/login/"

with self.settings(WEBAPP_LOGIN_URL=webapp_login_url):
response = self.client.post(
"/webapp/api/channels/",
{"channel_name": "test"},
)
self.assertEqual(response.status_code, 401)
self.assertEqual(
response.json(),
{"detail": "Authentication credentials were not provided."},
)
35 changes: 5 additions & 30 deletions tests/wagtail_live/webapp/test_channels.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Webapp Channel test suite """

from django.contrib.auth.models import User
from django.test import TestCase

from wagtail_live.webapp.models import Channel
Expand All @@ -15,39 +16,33 @@ def setUpTestData(cls):
Channel.objects.create(
channel_name=f"channel_{i}",
)
cls.user = User.objects.create(username="Tester")

def setUp(self):
self.client.force_login(self.user)


class ChannelViewsTests(ChannelTestCaseSetUp):
def test_channels_listing_status_code(self):
"""Response is 200 OK."""

response = self.client.get("/webapp/channels/")
self.assertEqual(response.status_code, 200)

def test_channels_listing_count(self):
"""Rendered context contains channels_count channels."""

response = self.client.get("/webapp/channels/")
self.assertEqual(len(response.context["channels"]), self.channels_count)

def test_channel_listing_status_code(self):
"""Response is 200 OK."""

response = self.client.get("/webapp/channels/channel_3/")
self.assertEqual(response.status_code, 200)

def test_retrieve_channel(self):
"""Response contains expected channel."""

response = self.client.get("/webapp/channels/channel_1/")
channel_exp = Channel.objects.get(channel_name="channel_1")
channel_got = response.context["channel"]

self.assertEqual(channel_exp, channel_got)

def test_retrieve_non_existent_channel(self):
"""Response is 404 Not Found."""

response = self.client.get("/webapp/channels/non_existent/")
self.assertEqual(response.status_code, 404)

Expand All @@ -71,67 +66,47 @@ def delete_channel(self, channel_name):
return response

def test_queryset_order_is_reversed(self):
"""Queryset order is reversed."""

first_channel = Channel.objects.get(channel_name="channel_1")
self.assertEqual(Channel.objects.last(), first_channel)

def test_create_channel_status_code(self):
"""Response is 201 CREATED."""

response = self.create_channel(channel_name="channel_6")
self.assertEqual(response.status_code, 201)

def test_create_channel_returns_new_channel_name(self):
"""Response contains new channel name."""

response = self.create_channel(channel_name="new_channel_name")
self.assertEqual(response.json()["channel_name"], "new_channel_name")

def test_channels_count_after_channel_creation(self):
"""Channels count has increased by 1."""

self.create_channel(channel_name="new_channel")
self.assertEqual(Channel.objects.count(), self.channels_count + 1)

def test_query_created_channel(self):
"""Created channel is stored."""

self.create_channel(channel_name="new_channel")
new_channel = Channel.objects.get(channel_name="new_channel")
self.assertEqual(new_channel.channel_name, "new_channel")

def test_create_channel_with_already_existing_channel(self):
"""New channel isn't created. 400 Bad Request"""

response = self.create_channel("channel_1")
self.assertEqual(response.status_code, 400)
self.assertEqual(Channel.objects.count(), self.channels_count)

def test_delete_channel_status_code(self):
"""Response is 204 DELETED."""

response = self.delete_channel(channel_name="channel_4")
self.assertEqual(response.status_code, 204)

def test_channels_count_after_channel_delete(self):
"""Channels count has decreased by 1."""

self.delete_channel(channel_name="channel_2")
self.assertEqual(Channel.objects.count(), self.channels_count - 1)

def test_query_deleted_channel(self):
"""Deleted channel does no longer exist."""

channel_to_delete = "channel_1"
msg = "Channel matching query does not exist"
with self.assertRaisesMessage(Channel.DoesNotExist, msg):
self.delete_channel(channel_name=channel_to_delete)
Channel.objects.get(channel_name=channel_to_delete)

def test_delete_non_existent_channel(self):
"""No channel is deleted. 404 Not Found"""

response = self.delete_channel(channel_name="non_existent_channel")
self.assertEqual(response.status_code, 404)
self.assertEqual(Channel.objects.count(), self.channels_count)
4 changes: 4 additions & 0 deletions tests/wagtail_live/webapp/test_images.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Webapp Image test suite """

from django.contrib.auth.models import User
from django.test import TestCase

from tests.utils import get_test_image_file
Expand All @@ -13,8 +14,11 @@ def setUp(self):

image_content = get_test_image_file()
self.image = Image.objects.create(message=message, image=image_content)
self.user = User.objects.create(username="Tester")
self.client.force_login(self.user)

def tearDown(self):
self.client.logout()
self.image.image.delete()

def test_delete_image(self):
Expand Down
Loading