Files
zulip/zerver/tests/test_custom_profile_data.py
Yashashvi Dave d7ee2aced1 models: Add external_account in custom profile field types.
Add new custom profile field type, External account.
External account field links user's social media
profile with account. e.g. GitHub, Twitter, etc.

Fixes part of #12302
2019-07-09 17:21:54 -07:00

626 lines
27 KiB
Python

# -*- coding: utf-8 -*-
from typing import Union, List, Dict, Any
from zerver.lib.actions import try_add_realm_custom_profile_field, \
do_update_user_custom_profile_data, do_remove_realm_custom_profile_field, \
try_reorder_realm_custom_profile_fields
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.bugdown import convert as bugdown_convert
from zerver.models import CustomProfileField, \
custom_profile_fields_for_realm, CustomProfileFieldValue, get_realm
import ujson
class CustomProfileFieldTest(ZulipTestCase):
def setUp(self) -> None:
self.realm = get_realm("zulip")
self.original_count = len(custom_profile_fields_for_realm(self.realm.id))
def custom_field_exists_in_realm(self, field_id: int) -> bool:
fields = custom_profile_fields_for_realm(self.realm.id)
field_ids = [field.id for field in fields]
return (field_id in field_ids)
def test_list(self) -> None:
self.login(self.example_email("iago"))
result = self.client_get("/json/realm/profile_fields")
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
content = result.json()
self.assertEqual(len(content["custom_fields"]), self.original_count)
def test_list_order(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
order = (
CustomProfileField.objects.filter(realm=realm)
.order_by('-order')
.values_list('order', flat=True)
)
try_reorder_realm_custom_profile_fields(realm, order)
result = self.client_get("/json/realm/profile_fields")
content = result.json()
self.assertListEqual(content["custom_fields"],
sorted(content["custom_fields"], key=lambda x: -x["id"]))
def test_create(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
data = {"name": u"Phone", "field_type": "text id"} # type: Dict[str, Any]
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, u'Argument "field_type" is not valid JSON.')
data["name"] = ""
data["field_type"] = 100
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, u'Name cannot be blank.')
data["name"] = "*" * 41
data["field_type"] = 100
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'name is too long (limit: 40 characters)')
data["name"] = "Phone"
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, u'Invalid field type.')
data["name"] = "Phone"
data["hint"] = "*" * 81
data["field_type"] = CustomProfileField.SHORT_TEXT
result = self.client_post("/json/realm/profile_fields", info=data)
msg = "hint is too long (limit: 80 characters)"
self.assert_json_error(result, msg)
data["name"] = "Phone"
data["hint"] = "Contact number"
data["field_type"] = CustomProfileField.SHORT_TEXT
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
field = CustomProfileField.objects.get(name="Phone", realm=realm)
self.assertEqual(field.id, field.order)
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result,
u'A field with that name already exists.')
def test_create_choice_field(self) -> None:
self.login(self.example_email("iago"))
data = {} # type: Dict[str, Union[str, int]]
data["name"] = "Favorite programming language"
data["field_type"] = CustomProfileField.CHOICE
data['field_data'] = 'invalid'
result = self.client_post("/json/realm/profile_fields", info=data)
error_msg = "Bad value for 'field_data': invalid"
self.assert_json_error(result, error_msg)
data["field_data"] = ujson.dumps({
'python': ['1'],
'java': ['2'],
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data is not a dict')
data["field_data"] = ujson.dumps({
'python': {'text': 'Python'},
'java': {'text': 'Java'},
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, "order key is missing from field_data")
data["field_data"] = ujson.dumps({
'python': {'text': 'Python', 'order': ''},
'java': {'text': 'Java', 'order': '2'},
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data["order"] cannot be blank.')
data["field_data"] = ujson.dumps({
'': {'text': 'Python', 'order': '1'},
'java': {'text': 'Java', 'order': '2'},
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, "'value' cannot be blank.")
data["field_data"] = ujson.dumps({
'python': {'text': 'Python', 'order': 1},
'java': {'text': 'Java', 'order': '2'},
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data["order"] is not a string')
data["field_data"] = ujson.dumps({})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'Field must have at least one choice.')
data["field_data"] = ujson.dumps({
'python': {'text': 'Python', 'order': '1'},
'java': {'text': 'Java', 'order': '2'},
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
def test_create_external_account_field(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
data = {} # type: Dict[str, Union[str, int, Dict[str, str]]]
data["name"] = "Twitter"
data["field_type"] = CustomProfileField.EXTERNAL_ACCOUNT
data['field_data'] = 'invalid'
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, "Bad value for 'field_data': invalid")
data['field_data'] = ujson.dumps({})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, "subtype key is missing from field_data")
data["field_data"] = ujson.dumps({
'subtype': ''
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data["subtype"] cannot be blank.')
data["field_data"] = ujson.dumps({
'subtype': '123'
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'Invalid external account type')
non_default_external_account = 'linkedin'
data["field_data"] = ujson.dumps({
'subtype': non_default_external_account
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'Invalid external account type')
data["field_data"] = ujson.dumps({
'subtype': 'twitter'
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
twitter_field = CustomProfileField.objects.get(name="Twitter", realm=realm)
self.assertEqual(twitter_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT)
self.assertEqual(twitter_field.name, "Twitter")
self.assertEqual(ujson.loads(twitter_field.field_data)['subtype'], 'twitter')
data['name'] = 'Reddit'
data["field_data"] = ujson.dumps({
'subtype': 'custom'
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'Custom external account must define url pattern')
data["field_data"] = ujson.dumps({
'subtype': 'custom',
'url_pattern': 123,
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data["url_pattern"] is not a string')
data["field_data"] = ujson.dumps({
'subtype': 'custom',
'url_pattern': 'invalid',
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'username should appear exactly once in pattern.')
data["field_data"] = ujson.dumps({
'subtype': 'custom',
'url_pattern': 'https://www.reddit.com/%(username)s/user/%(username)s',
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'username should appear exactly once in pattern.')
data["field_data"] = ujson.dumps({
'subtype': 'custom',
'url_pattern': 'reddit.com/%(username)s',
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, 'field_data["url_pattern"] is not a URL')
data["field_data"] = ujson.dumps({
'subtype': 'custom',
'url_pattern': 'https://www.reddit.com/user/%(username)s',
})
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
custom_field = CustomProfileField.objects.get(name="Reddit", realm=realm)
self.assertEqual(custom_field.field_type, CustomProfileField.EXTERNAL_ACCOUNT)
self.assertEqual(custom_field.name, "Reddit")
field_data = ujson.loads(custom_field.field_data)
self.assertEqual(field_data['subtype'], 'custom')
self.assertEqual(field_data['url_pattern'], 'https://www.reddit.com/user/%(username)s')
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_error(result, "A field with that name already exists.")
def test_not_realm_admin(self) -> None:
self.login(self.example_email("hamlet"))
result = self.client_post("/json/realm/profile_fields")
self.assert_json_error(result, u'Must be an organization administrator')
result = self.client_delete("/json/realm/profile_fields/1")
self.assert_json_error(result, 'Must be an organization administrator')
def test_delete(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
field = CustomProfileField.objects.get(name="Phone number", realm=realm)
result = self.client_delete("/json/realm/profile_fields/100")
self.assert_json_error(result, 'Field id 100 not found.')
self.assertTrue(self.custom_field_exists_in_realm(field.id))
result = self.client_delete(
"/json/realm/profile_fields/{}".format(field.id))
self.assert_json_success(result)
self.assertFalse(self.custom_field_exists_in_realm(field.id))
def test_update(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
result = self.client_patch(
"/json/realm/profile_fields/100",
info={'name': '',
'field_type': CustomProfileField.SHORT_TEXT}
)
self.assert_json_error(result, u'Name cannot be blank.')
result = self.client_patch(
"/json/realm/profile_fields/100",
info={'name': 'Phone Number',
'field_type': CustomProfileField.SHORT_TEXT}
)
self.assert_json_error(result, u'Field id 100 not found.')
field = CustomProfileField.objects.get(name="Phone number", realm=realm)
self.assertEqual(CustomProfileField.objects.count(), self.original_count)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'New phone number',
'field_type': CustomProfileField.SHORT_TEXT})
self.assert_json_success(result)
field = CustomProfileField.objects.get(id=field.id, realm=realm)
self.assertEqual(CustomProfileField.objects.count(), self.original_count)
self.assertEqual(field.name, 'New phone number')
self.assertIs(field.hint, '')
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': '*' * 41,
'field_type': CustomProfileField.SHORT_TEXT})
msg = "name is too long (limit: 40 characters)"
self.assert_json_error(result, msg)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'New phone number',
'hint': '*' * 81,
'field_type': CustomProfileField.SHORT_TEXT})
msg = "hint is too long (limit: 80 characters)"
self.assert_json_error(result, msg)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'New phone number',
'hint': 'New contact number',
'field_type': CustomProfileField.SHORT_TEXT})
self.assert_json_success(result)
field = CustomProfileField.objects.get(id=field.id, realm=realm)
self.assertEqual(CustomProfileField.objects.count(), self.original_count)
self.assertEqual(field.name, 'New phone number')
self.assertEqual(field.hint, 'New contact number')
self.assertEqual(field.field_type, CustomProfileField.SHORT_TEXT)
field = CustomProfileField.objects.get(name="Favorite editor", realm=realm)
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'Favorite editor',
'field_data': 'invalid'})
self.assert_json_error(result, "Bad value for 'field_data': invalid")
field_data = ujson.dumps({
'vim': 'Vim',
'emacs': {'order': '2', 'text': 'Emacs'},
})
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'Favorite editor',
'field_data': field_data})
self.assert_json_error(result, "field_data is not a dict")
field_data = ujson.dumps({
'vim': {'order': '1', 'text': 'Vim'},
'emacs': {'order': '2', 'text': 'Emacs'},
'notepad': {'order': '3', 'text': 'Notepad'},
})
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field.id),
info={'name': 'Favorite editor',
'field_data': field_data})
self.assert_json_success(result)
def test_update_is_aware_of_uniqueness(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
field_1 = try_add_realm_custom_profile_field(realm, u"Phone",
CustomProfileField.SHORT_TEXT)
field_2 = try_add_realm_custom_profile_field(realm, u"Phone 1",
CustomProfileField.SHORT_TEXT)
self.assertTrue(self.custom_field_exists_in_realm(field_1.id))
self.assertTrue(self.custom_field_exists_in_realm(field_2.id))
result = self.client_patch(
"/json/realm/profile_fields/{}".format(field_2.id),
info={'name': 'Phone', 'field_type': CustomProfileField.SHORT_TEXT})
self.assert_json_error(
result, u'A field with that name already exists.')
def assert_error_update_invalid_value(self, field_name: str, new_value: object, error_msg: str) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
field = CustomProfileField.objects.get(name=field_name, realm=realm)
# Update value of field
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps([{"id": field.id, "value": new_value}])})
self.assert_json_error(result, error_msg)
def test_reorder(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
order = (
CustomProfileField.objects.filter(realm=realm)
.order_by('-order')
.values_list('order', flat=True)
)
result = self.client_patch("/json/realm/profile_fields",
info={'order': ujson.dumps(order)})
self.assert_json_success(result)
fields = CustomProfileField.objects.filter(realm=realm).order_by('order')
for field in fields:
self.assertEqual(field.id, order[field.order])
def test_reorder_duplicates(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
order = (
CustomProfileField.objects.filter(realm=realm)
.order_by('-order')
.values_list('order', flat=True)
)
order = list(order)
order.append(4)
result = self.client_patch("/json/realm/profile_fields",
info={'order': ujson.dumps(order)})
self.assert_json_success(result)
fields = CustomProfileField.objects.filter(realm=realm).order_by('order')
for field in fields:
self.assertEqual(field.id, order[field.order])
def test_reorder_unauthorized(self) -> None:
self.login(self.example_email("hamlet"))
realm = get_realm('zulip')
order = (
CustomProfileField.objects.filter(realm=realm)
.order_by('-order')
.values_list('order', flat=True)
)
result = self.client_patch("/json/realm/profile_fields",
info={'order': ujson.dumps(order)})
self.assert_json_error(result, "Must be an organization administrator")
def test_reorder_invalid(self) -> None:
self.login(self.example_email("iago"))
order = [100, 200, 300]
result = self.client_patch("/json/realm/profile_fields",
info={'order': ujson.dumps(order)})
self.assert_json_error(
result, u'Invalid order mapping.')
order = [1, 2]
result = self.client_patch("/json/realm/profile_fields",
info={'order': ujson.dumps(order)})
self.assert_json_error(
result, u'Invalid order mapping.')
def test_update_invalid_field(self) -> None:
self.login(self.example_email("iago"))
data = [{'id': 1234, 'value': '12'}]
result = self.client_patch("/json/users/me/profile_data", {
'data': ujson.dumps(data)
})
self.assert_json_error(result,
u"Field id 1234 not found.")
def test_delete_field_value(self) -> None:
iago = self.example_user("iago")
self.login(iago.email)
realm = get_realm("zulip")
invalid_field_id = 1234
result = self.client_delete("/json/users/me/profile_data", {
'data': ujson.dumps([invalid_field_id])
})
self.assert_json_error(result,
u'Field id %d not found.' % (invalid_field_id,))
field = CustomProfileField.objects.get(name="Mentor", realm=realm)
data = [{'id': field.id,
'value': [self.example_user("aaron").id]}] # type: List[Dict[str, Union[int, str, List[int]]]]
do_update_user_custom_profile_data(iago, data)
iago_value = CustomProfileFieldValue.objects.get(user_profile=iago, field=field)
converter = field.FIELD_CONVERTERS[field.field_type]
self.assertEqual([self.example_user("aaron").id], converter(iago_value.value))
result = self.client_delete("/json/users/me/profile_data", {
'data': ujson.dumps([field.id])
})
self.assert_json_success(result)
# Don't throw an exception here
result = self.client_delete("/json/users/me/profile_data", {
'data': ujson.dumps([field.id])
})
self.assert_json_success(result)
def test_update_invalid_short_text(self) -> None:
field_name = "Phone number"
self.assert_error_update_invalid_value(field_name, 't' * 201,
u"{} is too long (limit: 50 characters)".format(field_name))
def test_update_invalid_date(self) -> None:
field_name = "Birthday"
self.assert_error_update_invalid_value(field_name, u"a-b-c",
u"{} is not a date".format(field_name))
self.assert_error_update_invalid_value(field_name, 123,
u"{} is not a string".format(field_name))
def test_update_invalid_url(self) -> None:
field_name = "Favorite website"
self.assert_error_update_invalid_value(field_name, u"not URL",
u"{} is not a URL".format(field_name))
def test_update_invalid_user_field(self) -> None:
field_name = "Mentor"
invalid_user_id = 1000
self.assert_error_update_invalid_value(field_name, [invalid_user_id],
u"Invalid user ID: %d"
% (invalid_user_id,))
def test_create_field_of_type_user(self) -> None:
self.login(self.example_email("iago"))
data = {"name": "Your mentor",
"field_type": CustomProfileField.USER,
}
result = self.client_post("/json/realm/profile_fields", info=data)
self.assert_json_success(result)
def test_update_profile_data_successfully(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
fields = [
('Phone number', '*short* text data'),
('Biography', '~~short~~ **long** text data'),
('Favorite food', 'long short text data'),
('Favorite editor', 'vim'),
('Birthday', '1909-3-5'),
('Favorite website', 'https://zulipchat.com'),
('Mentor', [self.example_user("cordelia").id]),
('GitHub', 'zulip-mobile')
]
data = []
for i, field_value in enumerate(fields):
name, value = field_value
field = CustomProfileField.objects.get(name=name, realm=realm)
data.append({
'id': field.id,
'value': value,
'field': field,
})
# Update value of field
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps(data)})
self.assert_json_success(result)
iago = self.example_user('iago')
expected_value = {f['id']: f['value'] for f in data}
expected_rendered_value = {} # type: Dict[Union[int, float, str, None], Union[str, None]]
for f in data:
if f['field'].is_renderable():
expected_rendered_value[f['id']] = bugdown_convert(f['value'])
else:
expected_rendered_value[f['id']] = None
for field_dict in iago.profile_data:
self.assertEqual(field_dict['value'], expected_value[field_dict['id']])
self.assertEqual(field_dict['rendered_value'], expected_rendered_value[field_dict['id']])
for k in ['id', 'type', 'name', 'field_data']:
self.assertIn(k, field_dict)
# Update value of one field.
field = CustomProfileField.objects.get(name='Biography', realm=realm)
data = [{
'id': field.id,
'value': 'foobar',
}]
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps(data)})
self.assert_json_success(result)
for f in iago.profile_data:
if f['id'] == field.id:
self.assertEqual(f['value'], 'foobar')
def test_update_invalid_choice_field(self) -> None:
field_name = "Favorite editor"
self.assert_error_update_invalid_value(field_name, "foobar",
"'foobar' is not a valid choice for '{}'.".format(field_name))
def test_update_choice_field_successfully(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
field = CustomProfileField.objects.get(name='Favorite editor', realm=realm)
data = [{
'id': field.id,
'value': 'emacs',
}]
result = self.client_patch("/json/users/me/profile_data",
{'data': ujson.dumps(data)})
self.assert_json_success(result)
def test_delete_internals(self) -> None:
user_profile = self.example_user('iago')
realm = user_profile.realm
field = CustomProfileField.objects.get(name="Phone number", realm=realm)
data = [{'id': field.id, 'value': u'123456'}] # type: List[Dict[str, Union[int, str, List[int]]]]
do_update_user_custom_profile_data(user_profile, data)
self.assertTrue(self.custom_field_exists_in_realm(field.id))
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), self.original_count)
do_remove_realm_custom_profile_field(realm, field)
self.assertFalse(self.custom_field_exists_in_realm(field.id))
self.assertEqual(user_profile.customprofilefieldvalue_set.count(), self.original_count - 1)
def test_null_value_and_rendered_value(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm("zulip")
quote = try_add_realm_custom_profile_field(
realm=realm,
name="Quote",
hint="Saying or phrase which you known for.",
field_type=CustomProfileField.SHORT_TEXT
)
iago = self.example_user("iago")
iago_profile_quote = iago.profile_data[-1]
value = iago_profile_quote["value"]
rendered_value = iago_profile_quote["rendered_value"]
self.assertIsNone(value)
self.assertIsNone(rendered_value)
update_dict = {
"id": quote.id,
"value": "***beware*** of jealousy..."
}
do_update_user_custom_profile_data(iago, [update_dict])
iago_profile_quote = self.example_user("iago").profile_data[-1]
value = iago_profile_quote["value"]
rendered_value = iago_profile_quote["rendered_value"]
self.assertIsNotNone(value)
self.assertIsNotNone(rendered_value)
self.assertEqual("<p><strong><em>beware</em></strong> of jealousy...</p>", rendered_value)