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-08-07 01:09:47 +02:00
|
|
|
import orjson
|
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')
|
|
|
|
|
2020-09-02 08:14:51 +02:00
|
|
|
extra_data: Dict[str, Any] = {}
|
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)
|
2020-08-07 01:09:47 +02:00
|
|
|
payload['widget_content'] = orjson.dumps(bogus_data).decode()
|
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-08-07 01:09:47 +02:00
|
|
|
widget_content=orjson.dumps(widget_content).decode(),
|
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')
|
2020-08-07 01:09:47 +02:00
|
|
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
2018-08-23 15:19:43 +02:00
|
|
|
|
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')
|
2020-08-07 01:09:47 +02:00
|
|
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
2018-08-23 14:40:24 +02:00
|
|
|
|
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')
|
2020-08-07 01:09:47 +02:00
|
|
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
2018-08-22 20:38:34 +02:00
|
|
|
|
|
|
|
# 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')
|
2020-08-07 01:09:47 +02:00
|
|
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|