2020-06-21 02:36:20 +02:00
|
|
|
import re
|
2020-06-11 00:54:34 +02:00
|
|
|
from typing import Any, Dict
|
2018-05-21 15:23:46 +02:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
import ujson
|
2020-06-21 02:36:20 +02:00
|
|
|
from django.core.exceptions import ValidationError
|
2018-08-22 20:38:34 +02:00
|
|
|
|
2018-05-21 15:23:46 +02:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.validator import check_widget_content
|
2018-08-23 14:51:17 +02:00
|
|
|
from zerver.lib.widget import get_widget_data
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.models import SubMessage
|
2018-08-23 14:51:17 +02:00
|
|
|
|
2018-05-21 15:23:46 +02:00
|
|
|
|
|
|
|
class WidgetContentTestCase(ZulipTestCase):
|
|
|
|
def test_validation(self) -> None:
|
|
|
|
def assert_error(obj: object, msg: str) -> None:
|
2020-06-21 02:36:20 +02:00
|
|
|
with self.assertRaisesRegex(ValidationError, re.escape(msg)):
|
|
|
|
check_widget_content(obj)
|
2018-05-21 15:23:46 +02:00
|
|
|
|
|
|
|
assert_error(5,
|
|
|
|
'widget_content is not a dict')
|
|
|
|
|
|
|
|
assert_error({},
|
|
|
|
'widget_type is not in widget_content')
|
|
|
|
|
|
|
|
assert_error(dict(widget_type='whatever'),
|
|
|
|
'extra_data is not in widget_content')
|
|
|
|
|
|
|
|
assert_error(dict(widget_type='zform', extra_data=4),
|
|
|
|
'extra_data is not a dict')
|
|
|
|
|
|
|
|
assert_error(dict(widget_type='bogus', extra_data={}),
|
|
|
|
'unknown widget type: bogus')
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
extra_data: Dict[str, Any] = dict()
|
2018-05-21 15:23:46 +02:00
|
|
|
obj = dict(widget_type='zform', extra_data=extra_data)
|
|
|
|
|
|
|
|
assert_error(obj, 'zform is missing type field')
|
|
|
|
|
|
|
|
extra_data['type'] = 'bogus'
|
|
|
|
assert_error(obj, 'unknown zform type: bogus')
|
|
|
|
|
|
|
|
extra_data['type'] = 'choices'
|
|
|
|
assert_error(obj, 'heading key is missing from extra_data')
|
|
|
|
|
|
|
|
extra_data['heading'] = 'whatever'
|
|
|
|
assert_error(obj, 'choices key is missing from extra_data')
|
|
|
|
|
|
|
|
extra_data['choices'] = 99
|
|
|
|
assert_error(obj, 'extra_data["choices"] is not a list')
|
|
|
|
|
|
|
|
extra_data['choices'] = [99]
|
|
|
|
assert_error(obj, 'extra_data["choices"][0] is not a dict')
|
|
|
|
|
|
|
|
extra_data['choices'] = [
|
|
|
|
dict(long_name='foo', reply='bar'),
|
|
|
|
]
|
|
|
|
assert_error(obj, 'short_name key is missing from extra_data["choices"][0]')
|
|
|
|
|
|
|
|
extra_data['choices'] = [
|
|
|
|
dict(short_name='a', long_name='foo', reply='bar'),
|
|
|
|
]
|
|
|
|
|
2020-06-21 02:36:20 +02:00
|
|
|
check_widget_content(obj)
|
2018-08-13 16:55:14 +02:00
|
|
|
|
|
|
|
def test_message_error_handling(self) -> None:
|
2020-03-10 11:48:26 +01:00
|
|
|
sender = self.example_user('cordelia')
|
2018-08-13 16:55:14 +02:00
|
|
|
stream_name = 'Verona'
|
|
|
|
|
|
|
|
payload = dict(
|
|
|
|
type="stream",
|
|
|
|
to=stream_name,
|
|
|
|
client='test suite',
|
2018-11-10 17:29:23 +01:00
|
|
|
topic='whatever',
|
2018-08-13 16:55:14 +02:00
|
|
|
content='whatever',
|
|
|
|
)
|
|
|
|
|
|
|
|
payload['widget_content'] = '{{{{{{' # unparsable
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-13 16:55:14 +02:00
|
|
|
self.assert_json_error_contains(result, 'Widgets: API programmer sent invalid JSON')
|
|
|
|
|
|
|
|
bogus_data = dict(color='red', foo='bar', x=2)
|
|
|
|
payload['widget_content'] = ujson.dumps(bogus_data)
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-13 16:55:14 +02:00
|
|
|
self.assert_json_error_contains(result, 'Widgets: widget_type is not in widget_content')
|
2018-08-22 20:38:34 +02:00
|
|
|
|
2018-08-23 14:51:17 +02:00
|
|
|
def test_get_widget_data_for_non_widget_messages(self) -> None:
|
|
|
|
# This is a pretty important test, despite testing the
|
|
|
|
# "negative" case. We never want widgets to interfere
|
|
|
|
# with normal messages.
|
|
|
|
|
|
|
|
test_messages = [
|
|
|
|
'',
|
|
|
|
' ',
|
|
|
|
'this is an ordinary message',
|
|
|
|
'/bogus_command',
|
|
|
|
'/me shrugs',
|
|
|
|
'use /poll',
|
|
|
|
]
|
|
|
|
|
|
|
|
for message in test_messages:
|
|
|
|
self.assertEqual(get_widget_data(content=message), (None, None))
|
|
|
|
|
|
|
|
# Add a positive check for context
|
|
|
|
self.assertEqual(get_widget_data(content='/tictactoe'), ('tictactoe', None))
|
|
|
|
|
2018-08-23 15:19:43 +02:00
|
|
|
def test_explicit_widget_content(self) -> None:
|
|
|
|
# Users can send widget_content directly on messages
|
|
|
|
# using the `widget_content` field.
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
sender = self.example_user('cordelia')
|
2018-08-23 15:19:43 +02:00
|
|
|
stream_name = 'Verona'
|
|
|
|
content = 'does-not-matter'
|
|
|
|
zform_data = dict(
|
|
|
|
type='choices',
|
|
|
|
heading='Options:',
|
|
|
|
choices=[],
|
|
|
|
)
|
|
|
|
|
2020-06-25 01:28:06 +02:00
|
|
|
widget_content = dict(
|
|
|
|
widget_type='zform',
|
|
|
|
extra_data=zform_data,
|
2018-08-23 15:19:43 +02:00
|
|
|
)
|
|
|
|
|
2020-06-25 01:28:06 +02:00
|
|
|
check_widget_content(widget_content)
|
|
|
|
|
2018-08-23 15:19:43 +02:00
|
|
|
payload = dict(
|
|
|
|
type="stream",
|
|
|
|
to=stream_name,
|
|
|
|
client='test suite',
|
2018-11-10 17:29:23 +01:00
|
|
|
topic='whatever',
|
2018-08-23 15:19:43 +02:00
|
|
|
content=content,
|
2020-06-25 01:28:06 +02:00
|
|
|
widget_content=ujson.dumps(widget_content),
|
2018-08-23 15:19:43 +02:00
|
|
|
)
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-23 15:19:43 +02:00
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
message = self.get_last_message()
|
|
|
|
self.assertEqual(message.content, content)
|
|
|
|
|
|
|
|
expected_submessage_content = dict(
|
|
|
|
widget_type="zform",
|
|
|
|
extra_data=zform_data,
|
|
|
|
)
|
|
|
|
|
|
|
|
submessage = SubMessage.objects.get(message_id=message.id)
|
|
|
|
self.assertEqual(submessage.msg_type, 'widget')
|
|
|
|
self.assertEqual(ujson.loads(submessage.content), expected_submessage_content)
|
|
|
|
|
2018-08-23 14:40:24 +02:00
|
|
|
def test_tictactoe(self) -> None:
|
|
|
|
# The tictactoe widget is mostly useful as a code sample,
|
|
|
|
# and it also helps us get test coverage that could apply
|
|
|
|
# to future widgets.
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
sender = self.example_user('cordelia')
|
2018-08-23 14:40:24 +02:00
|
|
|
stream_name = 'Verona'
|
|
|
|
content = '/tictactoe'
|
|
|
|
|
|
|
|
payload = dict(
|
|
|
|
type="stream",
|
|
|
|
to=stream_name,
|
|
|
|
client='test suite',
|
2018-11-10 17:29:23 +01:00
|
|
|
topic='whatever',
|
2018-08-23 14:40:24 +02:00
|
|
|
content=content,
|
|
|
|
)
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-23 14:40:24 +02:00
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
message = self.get_last_message()
|
|
|
|
self.assertEqual(message.content, content)
|
|
|
|
|
|
|
|
expected_submessage_content = dict(
|
|
|
|
widget_type="tictactoe",
|
|
|
|
extra_data=None,
|
|
|
|
)
|
|
|
|
|
|
|
|
submessage = SubMessage.objects.get(message_id=message.id)
|
|
|
|
self.assertEqual(submessage.msg_type, 'widget')
|
|
|
|
self.assertEqual(ujson.loads(submessage.content), expected_submessage_content)
|
|
|
|
|
2018-08-22 20:38:34 +02:00
|
|
|
def test_poll_command_extra_data(self) -> None:
|
2020-03-10 11:48:26 +01:00
|
|
|
sender = self.example_user('cordelia')
|
2018-08-22 20:38:34 +02:00
|
|
|
stream_name = 'Verona'
|
2019-01-28 18:37:28 +01:00
|
|
|
# We test for both trailing and leading spaces, along with blank lines
|
|
|
|
# for the poll options.
|
|
|
|
content = '/poll What is your favorite color?\n\nRed\nGreen \n\n Blue\n - Yellow'
|
2018-08-22 20:38:34 +02:00
|
|
|
|
|
|
|
payload = dict(
|
|
|
|
type="stream",
|
|
|
|
to=stream_name,
|
|
|
|
client='test suite',
|
2018-11-10 17:29:23 +01:00
|
|
|
topic='whatever',
|
2018-08-22 20:38:34 +02:00
|
|
|
content=content,
|
|
|
|
)
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-22 20:38:34 +02:00
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
message = self.get_last_message()
|
|
|
|
self.assertEqual(message.content, content)
|
|
|
|
|
|
|
|
expected_submessage_content = dict(
|
|
|
|
widget_type="poll",
|
|
|
|
extra_data=dict(
|
2019-01-28 18:37:28 +01:00
|
|
|
options=['Red', 'Green', 'Blue', 'Yellow'],
|
2018-08-22 20:38:34 +02:00
|
|
|
question="What is your favorite color?",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
submessage = SubMessage.objects.get(message_id=message.id)
|
|
|
|
self.assertEqual(submessage.msg_type, 'widget')
|
|
|
|
self.assertEqual(ujson.loads(submessage.content), expected_submessage_content)
|
|
|
|
|
|
|
|
# Now don't supply a question.
|
|
|
|
|
|
|
|
content = '/poll'
|
|
|
|
payload['content'] = content
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(sender, "/api/v1/messages", payload)
|
2018-08-22 20:38:34 +02:00
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
expected_submessage_content = dict(
|
|
|
|
widget_type="poll",
|
|
|
|
extra_data=dict(
|
2019-01-28 18:37:28 +01:00
|
|
|
options=[],
|
|
|
|
question='',
|
2018-08-22 20:38:34 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
message = self.get_last_message()
|
|
|
|
self.assertEqual(message.content, content)
|
|
|
|
submessage = SubMessage.objects.get(message_id=message.id)
|
|
|
|
self.assertEqual(submessage.msg_type, 'widget')
|
|
|
|
self.assertEqual(ujson.loads(submessage.content), expected_submessage_content)
|