From: mar77i Date: Sun, 20 Oct 2024 17:08:44 +0000 (+0200) Subject: improve tests. update todo X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=5e54052466648c874c0cd6802700b9979c510357;p=chat improve tests. update todo --- diff --git a/chat/rest_views.py b/chat/rest_views.py index fc1f0cd..c2c5474 100644 --- a/chat/rest_views.py +++ b/chat/rest_views.py @@ -33,20 +33,25 @@ class ModelRestView(LoginRequiredMixin, View): def get_queryset(self): return QuerySet(self.serializer.model).all() + safe_chars = "/#%[]=:;$&()+,!?*@'~" + def paginate(self, queryset, to_json): query = self.request.GET.copy() - full_path = self.request.get_full_path() - pos = full_path.find("?") - if pos >= 0: - full_path = full_path[:pos] if "before" in query: queryset = queryset.filter(pk__lt=int(query["before"])) count = queryset.count() - queryset = queryset[max(count - settings.DEFAULT_PAGE_SIZE, 0) :] - if count > settings.DEFAULT_PAGE_SIZE: + if "since" in query: + queryset = queryset.filter(pk__gte=int(query["since"])) + page_size = queryset.count() + else: + page_size = settings.DEFAULT_PAGE_SIZE + queryset = queryset[max(count - page_size, 0) :] + if count > page_size: query["before"] = queryset[0].pk prev_url = self.request.build_absolute_uri( - urlunsplit(("", "", full_path, query.urlencode(), "")), + urlunsplit( + ("", "", self.request.path, query.urlencode(self.safe_chars), "") + ), ) else: prev_url = None @@ -112,7 +117,14 @@ class ModelRestView(LoginRequiredMixin, View): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() - return getattr(self, self.method_map[request.method.lower()])() + try: + return getattr(self, self.method_map[request.method.lower()])() + except Exception as e: + return HttpResponse( + json.dumps({type(e).__name__: e.args}), + content_type="application/json", + status=500, + ) @classmethod def get_urls(cls, name): @@ -129,6 +141,13 @@ class ModelRestView(LoginRequiredMixin, View): ), ) + def handle_no_permission(self): + return HttpResponse( + json.dumps({"Location": settings.LOGIN_URL}), + content_type="application/json", + status=401, + ) + class ListAllMixin: def paginate(self, queryset, to_json): @@ -213,6 +232,10 @@ class ChannelMessageRestView(ModelRestView): def get_queryset(self): queryset = super().get_queryset().filter(channel__users=self.request.user.pk) - if "channel_id" in self.request.GET: + if ( + self.request.method == "GET" + and self.request.resolver_match.url_name == "chat-channelmessage-list" + and "channel_id" in self.request.GET + ): queryset = queryset.filter(channel_id=self.request.GET["channel_id"]) return queryset.order_by("ts") diff --git a/chat/serializers.py b/chat/serializers.py index 11dddd5..f542b31 100644 --- a/chat/serializers.py +++ b/chat/serializers.py @@ -55,8 +55,10 @@ class ModelSerializer: # disallow reassigning FKs on update if instance.pk is not None: raise ValueError(f"Not allowed to update: {field_name}") - if not isinstance(value, int): - raise ValueError(f"Please just add the _id field: {field_name}") + if not field_name.endswith("_id") or not isinstance(value, int): + raise ValueError( + f"Please use fk_id fields with an id: {field_name}" + ) setattr(instance, field_name, value) else: setattr(instance, field_name, value) diff --git a/chat/static/chat/chat.js b/chat/static/chat/chat.js index 737ee08..267f888 100644 --- a/chat/static/chat/chat.js +++ b/chat/static/chat/chat.js @@ -11,6 +11,12 @@ } bottom_callback = no_bottom_callback; + function maybe_redirect(data) { + if (data.hasOwnProperty("Location")) { + location.href = data.Location; + } + } + function xhr(method, url, callback, data) { return _.xhr( method, @@ -22,20 +28,12 @@ "error": function (msg) { add_msg(null, "xhr_error", msg); }, - "load": ( - callback - ? function () { - callback(JSON.parse(this.responseText)); - } - : null - ), - "readystatechange": function () { - if (this.readyState === this.DONE) { - if (this.readyState === this.DONE) { - console.log("DONE url?", this.responseURL, url); - } else { - console.log("url?", this.responseURL, url); - } + "load": function () { + data = JSON.parse(this.responseText); + if (this.status === 401) { + maybe_redirect(data); + } else if (callback) { + callback(data); } } }, @@ -385,9 +383,7 @@ function ws_receive(msg) { var data = JSON.parse(msg.data); - if (data.hasOwnProperty("Location")) { - location.href = data.Location; - } + maybe_redirect(data); // here we could add indication for the pm or channel being modified if ( data.table === "chat_channel" || data.table === "chat_channeluser" @@ -402,8 +398,8 @@ } function send_msg() { - var data = {"text": ta.value}; var ta = _.dgEBCN0("messages_footer").children[0]; + var data = {"text": ta.value}; _.foreach_obj( current_channel.msg_data, function (name, value) { diff --git a/chat/static/chat/underscore.js b/chat/static/chat/underscore.js index 26d410f..3c074aa 100644 --- a/chat/static/chat/underscore.js +++ b/chat/static/chat/underscore.js @@ -58,7 +58,7 @@ } ); } - foreach_arr( + foreach_obj( callbacks, function (event_name, callback) { request.addEventListener(event_name, callback); diff --git a/chat/tests.py b/chat/tests.py index 5d1ea20..632031b 100644 --- a/chat/tests.py +++ b/chat/tests.py @@ -458,8 +458,8 @@ class ChatTest(ChatTestMixin, TestCase): user2_url = reverse("chat-user-detail", args=[user2.pk]) response = self.client.get(user2_url) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.headers["Location"], f"/login/?next={user2_url}") + self.assertEqual(response.status_code, 401) + self.assertEqual(json.loads(response.content), {"Location": "/login/"}) cookies = self.login_user(user2, user2_password) response = self.client.get(user2_url, headers={"cookie": cookies}) data = json.loads(response.content) @@ -596,13 +596,27 @@ class ChatTest(ChatTestMixin, TestCase): user3.is_staff = False user3.save(update_fields=["is_staff"]) self.assertFalse(user3.is_privileged()) + response = self.client.post( + reverse("chat-user-list"), + json.dumps({"username": "user5"}), + content_type="application/json", + headers={"cookie": cookies}, + ) + self.assertEqual(response.status_code, 401) + response = self.client.delete( + user4_url, + json.dumps({"username": "user5"}), + content_type="application/json", + headers={"cookie": cookies}, + ) + self.assertEqual(response.status_code, 401) response = self.client.put( user4_data["url"], json.dumps({"username": "user4_modified"}).encode(), content_type="application/json", headers={"cookie": cookies}, ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401) response = self.client.put( reverse("chat-user-detail", args=[user3.pk]), json.dumps({"username": "user3"}).encode(), @@ -656,8 +670,9 @@ class ChatTest(ChatTestMixin, TestCase): def test_chat_privatemessage_api(self): user1, user2, user2_password, channel = self.setup_users() cookies = self.login_user(user2, user2_password) + privatemessage_list_url = reverse("chat-privatemessage-list") response = self.client.post( - reverse("chat-privatemessage-list"), + privatemessage_list_url, json.dumps( { "id": None, @@ -669,12 +684,14 @@ class ChatTest(ChatTestMixin, TestCase): headers={"cookie": cookies}, ) data = json.loads(response.content) - privatemessage_url = reverse("chat-privatemessage-detail", args=[data["id"]]) + privatemessage_detail_url = reverse( + "chat-privatemessage-detail", args=[data["id"]] + ) self.assertEqual( data, { "id": data["id"], - "url": f"http://testserver{privatemessage_url}", + "url": f"http://testserver{privatemessage_detail_url}", "ts": data["ts"], "sender_id": user2.pk, "recipient_id": user1.pk, @@ -705,7 +722,7 @@ class ChatTest(ChatTestMixin, TestCase): data_new, { "id": data["id"], - "url": f"http://testserver{privatemessage_url}", + "url": f"http://testserver{privatemessage_detail_url}", "ts": data["ts"], "sender_id": user2.pk, "recipient_id": user1.pk, @@ -713,7 +730,7 @@ class ChatTest(ChatTestMixin, TestCase): }, ) response = self.client.get( - f"{reverse('chat-privatemessage-list')}?other={user1.pk}", + f"{reverse('chat-privatemessage-list')}?recipient_id={user1.pk}", headers={"cookie": cookies}, ) data = json.loads(response.content) @@ -733,7 +750,7 @@ class ChatTest(ChatTestMixin, TestCase): "result": [ { "id": data["result"][0]["id"], - "url": f"http://testserver{privatemessage_url}", + "url": f"http://testserver{privatemessage_detail_url}", "ts": data["result"][0]["ts"], "sender_id": user2.pk, "recipient_id": user1.pk, @@ -742,6 +759,24 @@ class ChatTest(ChatTestMixin, TestCase): ], }, ) + response = self.client.put( + privatemessage_detail_url, + json.dumps({"recipient_id": user2.pk}), + content_type="application/json", + ) + self.assertEqual( + json.loads(response.content), + {"ValueError": ["Not allowed to update: recipient_id"]}, + ) + response = self.client.post( + privatemessage_list_url, + json.dumps({"recipient_id": reverse("chat-user-detail", args=[user2.pk])}), + content_type="application/json", + ) + self.assertEqual( + json.loads(response.content), + {"ValueError": ["Please use fk_id fields with an id: recipient_id"]}, + ) def test_chat_channelmessage_api(self): user1, user2, user2_password, channel = self.setup_users() @@ -776,12 +811,20 @@ class ChatTest(ChatTestMixin, TestCase): user=user2, text=token_urlsafe(), ) - list_url = f"{reverse('chat-channelmessage-list')}?channel={channel.pk}" + list_url = f"{reverse('chat-channelmessage-list')}?channel_id={channel.pk}" response = self.client.get(list_url) data = json.loads(response.content) + first_page_ids = [item["id"] for item in data["result"]] first_id = data["result"][0]["id"] - list_url = f"{list_url}&before={first_id}" - self.assertEqual(data["previous"], f"http://testserver{list_url}") + before = f"&before={first_id}" + self.assertEqual(data["previous"], f"http://testserver{list_url}{before}") + response = self.client.get(data["previous"]) data = json.loads(response.content) self.assertTrue(all(item["id"] < first_id for item in data["result"])) + + response = self.client.get(f"{list_url}&since={data['result'][0]['id']}") + self.assertEqual( + [item["id"] for item in json.loads(response.content)["result"]], + [*(item["id"] for item in data["result"]), *first_page_ids], + ) diff --git a/chat/views.py b/chat/views.py index ee67f03..f366f4f 100644 --- a/chat/views.py +++ b/chat/views.py @@ -1,5 +1,7 @@ +from asgiref.sync import sync_to_async from django.conf import settings from django.contrib.auth.forms import SetPasswordForm +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import update_last_login from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.views import ( @@ -11,7 +13,7 @@ from django.contrib.auth.views import ( from django.contrib.staticfiles import finders from django.core.mail import mail_admins from django.core.signing import Signer -from django.http import FileResponse, Http404 +from django.http import FileResponse, Http404, HttpResponseBase from django.urls import reverse, reverse_lazy from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.safestring import mark_safe @@ -23,7 +25,13 @@ from .forms import AuthenticationForm, PasswordResetForm, RegisterForm from .models import User -class ChatView(View): +class ChatView(LoginRequiredMixin, View): + async def dispatch(self, request, *args, **kwargs): + result = sync_to_async(super().dispatch)(request, *args, **kwargs) + while not isinstance(result, HttpResponseBase): + result = await result + return result + async def get(self, request, *args, **kwargs): path = finders.find("chat/chat.html") with open(path) as fh: diff --git a/manage.py b/manage.py index ecd372e..3cec53a 100755 --- a/manage.py +++ b/manage.py @@ -19,12 +19,12 @@ def setup_virtual_env(): # pragma: no cover def main(): """Run administrative tasks.""" - if "VIRTUAL_ENV" not in os.environ: + if "VIRTUAL_ENV" not in os.environ: # pragma: no cover setup_virtual_env() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chat.settings") try: from django.core.management import execute_from_command_line - except ImportError as exc: + except ImportError as exc: # pragma: no cover raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " diff --git a/todo.txt b/todo.txt index 55cfb8b..f24a785 100644 --- a/todo.txt +++ b/todo.txt @@ -1,10 +1,12 @@ -[ ] make it easy to see what users are in the current channel +[ ] logout button +[ ] show message timestamps and message edit/delete menus + - privileged users can edit / delete any message +[ ] edit existing messages [ ] "load more" button [ ] new message(s) indicators in left panel -[ ] edit existing messages -[ ] channel urls, privatemessage urls -[ ] in , use javascript:void(0) instead +[ ] update address bar for different views [ ] channel management: channel admin +[ ] write email to user [ ] ws client infrastructure: auto-reconnect, notify-throttle [ ] server-side throttling @@ -14,4 +16,3 @@ [ ] ws and chat tests? [ ] media uploads: images, gifs, even video files? [ ] moderation functions: report messages to MANAGERS, including private ones -[ ] write email to user