From aa16944a592586bbebd24174d0a82cbc86e60804 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Sat, 24 Feb 2018 08:26:04 -0800 Subject: [PATCH] Wrap user-facing strings in gettext; add icon_emoji --- docs/intro.rst | 24 ++++------ emojiwatch/migrations/0001_initial.py | 9 ++-- emojiwatch/models.py | 42 +++++++++++++---- emojiwatch/views.py | 66 +++++++++++++++++--------- tests/test_models.py | 37 +++++++++++---- tests/test_views.py | 67 +++++++++++++++++++-------- 6 files changed, 166 insertions(+), 79 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index a6d3914..5160099 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -95,22 +95,16 @@ To override this, use the ``FERNET_KEYS`` setting. For example: from os import environ SECRET_KEY = environ['SECRET_KEY'] - # Use only Base64-encoded 32 byte values for keys; don't derive them - # from arbitrary strings + + # The keys as a single environment variable separated by semicolons. + # The first entry is the current key (for encrypting and + # decrypting). Any additional entries are older keys for decrypting + # only. + FERNET_KEYS = environ['FERNET_KEY'].split(';') + + # If the following is False, use only Base64-encoded 32 byte values + # for keys. Don't derive them from arbitrary strings. FERNET_USE_HKDF = False - # For supporting any legacy keys that were used when FERNET_USE_HKDF - # was True - from fernet_fields.hkdf import derive_fernet_key - # The keys - FERNET_KEYS = [ - # The first entry is the current key (for encrypting and - # decrypting) - environ['FERNET_KEY'], - # Optional additional entries are older keys for decrypting only - # environ['OLD_FERNET_KEY_1'], - # Equivalent to the default key - # derive_fernet_key(SECRET_KEY), - ] See `the docs `__ for details. diff --git a/emojiwatch/migrations/0001_initial.py b/emojiwatch/migrations/0001_initial.py index 63dede7..bb90ebc 100644 --- a/emojiwatch/migrations/0001_initial.py +++ b/emojiwatch/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.2 on 2018-02-23 08:04 +# Generated by Django 2.0.2 on 2018-02-25 02:47 import django.core.validators from django.db import migrations, models @@ -18,9 +18,10 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('_version', models.IntegerField(default=-2147483648)), - ('team_id', models.CharField(max_length=63, unique=True, validators=[django.core.validators.RegexValidator('^T[0-9A-Z]*$', message='Must be of the format (e.g.) T123ABC...')], verbose_name='Team ID')), - ('workspace_token', fernet_fields.fields.EncryptedCharField(max_length=255, validators=[django.core.validators.RegexValidator('^xoxa-([0-9A-Fa-f]+)+', message='Must be of the format (e.g.) xoxa-1f2e3d-4c5b6a...')], verbose_name='Workspace Token')), - ('channel_id', models.CharField(max_length=63, validators=[django.core.validators.RegexValidator('^C[0-9A-Z]*$', message='Must be of the format (e.g.) C123ABC...')], verbose_name='Channel ID')), + ('team_id', models.CharField(default='T', max_length=63, unique=True, validators=[django.core.validators.RegexValidator('\\AT[0-9A-Z]+\\Z', message='Must be of the format (e.g.) T123ABC...')], verbose_name='Team ID')), + ('auth_token', fernet_fields.fields.EncryptedCharField(default='xoxa-', max_length=255, validators=[django.core.validators.RegexValidator('\\Axox[abp]-([0-9A-Fa-f]+)+', message='Must be of the format (e.g.) xox...-1f2e3d-4c5b6a...')], verbose_name='OAuth Token')), + ('channel_id', models.CharField(default='C', max_length=63, validators=[django.core.validators.RegexValidator('\\AC[0-9A-Z]+\\Z', message='Must be of the format (e.g.) C123ABC...')], verbose_name='Channel ID')), + ('icon_emoji', models.CharField(default=':robot_face:', max_length=255, validators=[django.core.validators.RegexValidator('\\A:[\\w-]+:\\Z', message='Must be of the format (e.g.) :emoji_name:...')], verbose_name='Icon Emoji')), ('notes', fernet_fields.fields.EncryptedTextField(blank=True, default='', verbose_name='Notes')), ], options={ diff --git a/emojiwatch/models.py b/emojiwatch/models.py index 3b0136c..9d056b5 100644 --- a/emojiwatch/models.py +++ b/emojiwatch/models.py @@ -35,12 +35,15 @@ __all__ = () +AUTH_TOKEN_MAX_LEN = 255 +AUTH_TOKEN_RE = r'\Axox[abp]-([0-9A-Fa-f]+)+' CHANNEL_ID_MAX_LEN = 63 -CHANNEL_ID_RE = r'^C[0-9A-Z]*$' +CHANNEL_ID_RE = r'\AC[0-9A-Z]+\Z' +ICON_EMOJI_DEFAULT = ':robot_face:' +ICON_EMOJI_MAX_LEN = 255 +ICON_EMOJI_RE = r'\A:[\w-]+:\Z' TEAM_ID_MAX_LEN = 63 -TEAM_ID_RE = r'^T[0-9A-Z]*$' -WORKSPACE_TOKEN_MAX_LEN = 255 -WORKSPACE_TOKEN_RE = r'^xoxa-([0-9A-Fa-f]+)+' +TEAM_ID_RE = r'\AT[0-9A-Z]+\Z' # ---- Exceptions -------------------------------------------------------- @@ -96,7 +99,7 @@ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_updat updated = filtered._update(values) # pylint: disable=protected-access if updated == 0: - raise StaleVersionError('{!r}._version {} is stale'.format(self, self._version - 1)) + raise StaleVersionError(gettext('{!r}._version {} is stale').format(self, self._version - 1)) assert updated == 1 @@ -116,6 +119,7 @@ class Meta(object): # ---- Properties ---------------------------------------------------- team_id = d_d_models.CharField( + default='T', max_length=TEAM_ID_MAX_LEN, null=False, unique=True, @@ -127,16 +131,20 @@ class Meta(object): team_id.short_description = gettext('Team ID (e.g., T123ABC...)') - workspace_token = fernet_fields.EncryptedCharField( - max_length=WORKSPACE_TOKEN_MAX_LEN, + auth_token = fernet_fields.EncryptedCharField( + default='xoxa-', + max_length=AUTH_TOKEN_MAX_LEN, null=False, validators=[ - d_c_validators.RegexValidator(WORKSPACE_TOKEN_RE, message=gettext('Must be of the format (e.g.) xoxa-1f2e3d-4c5b6a...')), + d_c_validators.RegexValidator(AUTH_TOKEN_RE, message=gettext('Must be of the format (e.g.) xox...-1f2e3d-4c5b6a...')), ], - verbose_name=gettext('Workspace Token'), + verbose_name=gettext('OAuth Token'), ) + auth_token.short_description = gettext('Auth Token (e.g., a workspace token like xoxa-...)') + channel_id = d_d_models.CharField( + default='C', max_length=CHANNEL_ID_MAX_LEN, null=False, validators=[ @@ -145,6 +153,20 @@ class Meta(object): verbose_name=gettext('Channel ID'), ) + team_id.short_description = gettext('Channel ID (e.g., C123ABC...)') + + icon_emoji = d_d_models.CharField( + default=ICON_EMOJI_DEFAULT, + max_length=ICON_EMOJI_MAX_LEN, + null=False, + validators=[ + d_c_validators.RegexValidator(ICON_EMOJI_RE, message=gettext('Must be of the format (e.g.) :emoji_name:...')), + ], + verbose_name=gettext('Icon Emoji'), + ) + + icon_emoji.short_description = gettext('Icon Emoji (e.g., {}...)').format(ICON_EMOJI_DEFAULT) + notes = fernet_fields.EncryptedTextField( blank=True, default='', @@ -153,4 +175,4 @@ class Meta(object): @property def slack(self): - return slacker.Slacker(self.workspace_token) + return slacker.Slacker(self.auth_token) diff --git a/emojiwatch/views.py b/emojiwatch/views.py index 9e4f5e5..101e4ba 100644 --- a/emojiwatch/views.py +++ b/emojiwatch/views.py @@ -34,6 +34,8 @@ import django.views.decorators.http as d_v_d_http import django.views.generic as d_v_generic +from gettext import gettext + from . import ( LOGGER, SLACK_VERIFICATION_TOKEN, @@ -53,7 +55,7 @@ _EMOJI_URL_MAX_LEN = 1023 _EMOJIS_MAX_LEN = 32 _FIELD_MAX_LEN = 63 -_SHRUG = '\u00af\\_(\u30c4)_/\u00af' +_SHRUG = str(u'\u00af\\_(\u30c4)_/\u00af') _SUB_HANDLERS_BY_SUBTYPE = {} # type: typing.Dict[typing.Text, typing.Callable[[SlackWorkspaceEmojiWatcher, typing.Dict], slacker.Response]] _UNRECOGNIZED_JSON_BODY_ERR = 'unrecognized JSON structure from request body' @@ -71,7 +73,7 @@ class RequestPayloadValidationError(Exception): def __init__( # noqa:F811 # pylint: disable=keyword-arg-before-vararg self, - message=_UNRECOGNIZED_JSON_BODY_ERR, + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR), response=d_http.HttpResponseBadRequest(), *args, **kw @@ -121,7 +123,7 @@ def event_hook_handler( slack_retry_reason = request.META.get('HTTP_X_SLACK_RETRY_REASON', None) if slack_retry_num: - LOGGER.info("Slack retry attempt %s ('%s')", slack_retry_num, slack_retry_reason) + LOGGER.info(gettext("Slack retry attempt %s ('%s')"), slack_retry_num, slack_retry_reason) content_type = request.META.get('HTTP_CONTENT_TYPE', 'application/json') @@ -132,7 +134,7 @@ def event_hook_handler( try: payload_data = json.loads(request.body.decode('utf-8')) # type: typing.Dict except (JSONDecodeError, UnicodeDecodeError): - LOGGER.info('unable to parse JSON from request body') + LOGGER.info(gettext('unable to parse JSON from request body')) truncate_len = 1024 half_truncate_len = truncate_len >> 1 @@ -148,12 +150,12 @@ def event_hook_handler( verification_token = payload_data['token'] except (KeyError, TypeError): verification_token = None - LOGGER.info(_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'token')") # pylint: disable=logging-not-lazy + LOGGER.info(gettext(_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'token')")) # pylint: disable=logging-not-lazy if not verification_token \ or verification_token != SLACK_VERIFICATION_TOKEN: raise RequestPayloadValidationError( - message='bad verification token', + message=gettext('bad verification token'), response=d_http.HttpResponseForbidden(), ) @@ -161,7 +163,7 @@ def event_hook_handler( call_type = payload_data['type'] except KeyError: raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'type')", + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'type')"), ) if call_type == 'url_verification': @@ -169,20 +171,20 @@ def event_hook_handler( challenge = payload_data['challenge'] except KeyError: raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'challenge')", + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + " (missing 'challenge')"), ) if not isinstance(challenge, str) \ or len(challenge) > _CHALLENGE_MAX_LEN: raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized challenge)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized challenge)'), ) return d_http.HttpResponse(challenge, content_type='text/plain') if call_type != 'event_callback': raise RequestPayloadValidationError( - message='unrecognized call type', + message=gettext('unrecognized call type'), ) try: @@ -197,7 +199,7 @@ def event_hook_handler( or len(event_type) > _FIELD_MAX_LEN \ or event_type != 'emoji_changed': raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event type)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event type)'), ) try: @@ -208,21 +210,21 @@ def event_hook_handler( subhandler = _SUB_HANDLERS_BY_SUBTYPE[event_subtype] except (KeyError, ValueError): raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event subtype)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event subtype)'), ) if not isinstance(team_id, str) \ or len(team_id) > TEAM_ID_MAX_LEN \ or not re.search(TEAM_ID_RE, team_id): raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized team_id)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized team_id)'), ) team = SlackWorkspaceEmojiWatcher.objects.filter(team_id=team_id).first() if team is None: raise RequestPayloadValidationError( - message='no such team ({})'.format(team_id), + message=gettext('no such team ({})').format(team_id), ) try: @@ -231,13 +233,13 @@ def event_hook_handler( except slacker.Error as exc: if exc.args == ('invalid_auth',): raise RequestPayloadValidationError( - message='call to Slack API failed auth', + message=gettext('call to Slack API failed auth'), response=d_http.HttpResponseForbidden(), ) # Log, but otherwise ignore errors from our callbacks to Slack's # API - LOGGER.info('falled call to Slack') + LOGGER.info(gettext('falled call to Slack')) LOGGER.debug(_SHRUG, exc_info=True) except RequestPayloadValidationError as exc: @@ -250,6 +252,20 @@ def event_hook_handler( return d_http.HttpResponse() +# ======================================================================== +def _as_user( + team, # type: SlackWorkspaceEmojiWatcher +): # type: (...) -> typing.Optional[bool] + # as_user can only be non-None if it's a bot token (see + # ) + return False if re.search(r'\Axoxb-', team.auth_token) else None + +# ======================================================================== +def _icon_emoji( + team, # type: SlackWorkspaceEmojiWatcher +): # type: (...) -> typing.Optional[typing.Text] + return team.icon_emoji if team.icon_emoji else None + # ======================================================================== def _handle_add( team, # type: SlackWorkspaceEmojiWatcher @@ -262,13 +278,13 @@ def _handle_add( or not emoji_name \ or len(emoji_name) > _EMOJI_NAME_MAX_LEN: raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event name)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event name)'), ) if not isinstance(emoji_url, str) \ or len(emoji_url) > _EMOJI_URL_MAX_LEN: raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event value)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event value)'), ) if emoji_url: @@ -277,14 +293,16 @@ def _handle_add( 'fallback': '<{}>'.format(emoji_url), 'image_url': emoji_url, }, - ] # type: typing.Optional[typing.List[typing.Dict]] + ] # type: typing.Optional[typing.List[typing.Dict[typing.Text, typing.Text]]] else: attachments = None return team.slack.chat.post_message( team.channel_id, - html.escape('added `:{}:`').format(emoji_name), + html.escape(gettext('added `:{}:`').format(emoji_name)), + as_user=_as_user(team), attachments=attachments, + icon_emoji=_icon_emoji(team), ) # ======================================================================== @@ -296,7 +314,7 @@ def _handle_remove( if not isinstance(emoji_names, list): raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event names)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event names)'), ) if not emoji_names: @@ -308,12 +326,14 @@ def _handle_remove( if any((not isinstance(emoji_name, str) for emoji_name in emoji_names[:_EMOJIS_MAX_LEN])) \ or any((len(emoji_name) > _EMOJI_NAME_MAX_LEN for emoji_name in emoji_names)): raise RequestPayloadValidationError( - message=_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event names)', + message=gettext(_UNRECOGNIZED_JSON_BODY_ERR + ' (unrecognized event names)'), ) return team.slack.chat.post_message( team.channel_id, - html.escape('removed {}{}').format(', '.join('`:{}:`'.format(name) for name in emoji_names[:_EMOJIS_MAX_LEN]), '...' if too_many else ''), + html.escape(gettext('removed {}{}').format(', '.join('`:{}:`'.format(name) for name in emoji_names[:_EMOJIS_MAX_LEN]), '...' if too_many else '')), + as_user=_as_user(team), + icon_emoji=_icon_emoji(team), ) # ---- Initialization ---------------------------------------------------- diff --git a/tests/test_models.py b/tests/test_models.py index 9cf6457..1dd5651 100755 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -59,22 +59,38 @@ def test_form_validation(self): for field in ( 'team_id', - 'workspace_token', + 'auth_token', + 'channel_id', + ): + self.assertRegex(cm.exception.message_dict[field][0], r'\AMust be of the format \(e\.g\.\) ', msg='(field = {!r})'.format(field)) + + ws_emoji_watcher.team_id = '' + ws_emoji_watcher.auth_token = '' + ws_emoji_watcher.channel_id = '' + + with self.assertRaises(d_c_exceptions.ValidationError) as cm: + ws_emoji_watcher.full_clean() + + for field in ( + 'team_id', + 'auth_token', 'channel_id', ): self.assertEqual(cm.exception.message_dict[field][0], 'This field cannot be blank.', msg='(field = {!r})'.format(field)) ws_emoji_watcher.team_id = '...' - ws_emoji_watcher.workspace_token = '...' + ws_emoji_watcher.auth_token = '...' ws_emoji_watcher.channel_id = '...' + ws_emoji_watcher.icon_emoji = '...' with self.assertRaises(d_c_exceptions.ValidationError) as cm: ws_emoji_watcher.full_clean() for field, field_fmt in ( ('team_id', 'T123ABC'), - ('workspace_token', 'xoxa-1f2e3d-4c5b6a'), + ('auth_token', 'xox...-1f2e3d-4c5b6a'), ('channel_id', 'C123ABC'), + ('icon_emoji', ':emoji_name:'), ): field_fmt_re = r'\AMust be of the format \(e\.g\.\) {}\.\.\.\Z'.format(re.escape(field_fmt)) self.assertRegex(cm.exception.message_dict[field][0], field_fmt_re, msg='(field = {!r})'.format(field)) @@ -82,8 +98,9 @@ def test_form_validation(self): def test_create(self): ws_emoji_watcher1 = SlackWorkspaceEmojiWatcher() ws_emoji_watcher1.team_id = 'T123ABC' - ws_emoji_watcher1.workspace_token = 'xoxa-1f2e3d-4c5b6a' + ws_emoji_watcher1.auth_token = 'xoxa-1f2e3d-4c5b6a' ws_emoji_watcher1.channel_id = 'C123ABC' + ws_emoji_watcher1.icon_emoji = ':blush:' self.assertLess(ws_emoji_watcher1._version, 0) # pylint: disable=protected-access ws_emoji_watcher1.full_clean() @@ -96,8 +113,9 @@ def test_create(self): ws_emoji_watcher2 = SlackWorkspaceEmojiWatcher() ws_emoji_watcher2.team_id = 'T456DEF' - ws_emoji_watcher2.workspace_token = 'xoxa-4c5b6a-1f2e3d' + ws_emoji_watcher2.auth_token = 'xoxa-4c5b6a-1f2e3d' ws_emoji_watcher2.channel_id = 'C456DEF' + ws_emoji_watcher2.icon_emoji = ':smirk:' self.assertLess(ws_emoji_watcher2._version, 0) # pylint: disable=protected-access ws_emoji_watcher2.full_clean() @@ -111,14 +129,16 @@ def test_create(self): def test_team_id_uniqueness(self): ws_emoji_watcher1 = SlackWorkspaceEmojiWatcher() ws_emoji_watcher1.team_id = 'T123ABC' - ws_emoji_watcher1.workspace_token = 'xoxa-1f2e3d-4c5b6a' + ws_emoji_watcher1.auth_token = 'xoxa-1f2e3d-4c5b6a' ws_emoji_watcher1.channel_id = 'C123ABC' + ws_emoji_watcher1.icon_emoji = ':blush:' ws_emoji_watcher1.save() ws_emoji_watcher2 = SlackWorkspaceEmojiWatcher() ws_emoji_watcher2.team_id = ws_emoji_watcher1.team_id - ws_emoji_watcher2.workspace_token = 'xoxa-4c5b6a-1f2e3d' + ws_emoji_watcher2.auth_token = 'xoxa-4c5b6a-1f2e3d' ws_emoji_watcher2.channel_id = 'C456DEF' + ws_emoji_watcher2.icon_emoji = ':smirk:' self.assertLess(ws_emoji_watcher2._version, 0) # pylint: disable=protected-access old_version = ws_emoji_watcher2._version # pylint: disable=protected-access @@ -130,8 +150,9 @@ def test_team_id_uniqueness(self): def test_version_staleness(self): ws_emoji_watcher1 = SlackWorkspaceEmojiWatcher() ws_emoji_watcher1.team_id = 'T123ABC' - ws_emoji_watcher1.workspace_token = 'xoxa-1f2e3d-4c5b6a' + ws_emoji_watcher1.auth_token = 'xoxa-1f2e3d-4c5b6a' ws_emoji_watcher1.channel_id = 'C123ABC' + ws_emoji_watcher1.icon_emoji = ':blush:' ws_emoji_watcher1.save() ws_emoji_watcher2 = SlackWorkspaceEmojiWatcher.objects.filter(team_id=ws_emoji_watcher1.team_id).first() diff --git a/tests/test_views.py b/tests/test_views.py index 264b5bb..abbad7c 100755 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -108,8 +108,19 @@ class EventHandlerTestCaseBase(d_test.TestCase): # ---- Data ---------------------------------------------------------- - CLIENT_ID = 'C123ABC' - TEAM_ID = 'T123ABC' + WORKSPACE = { + 'team_id': 'T123ABC', + 'auth_token': 'xoxa-1f2e3d-4c5b6a', + 'channel_id': 'C123ABC', + 'icon_emoji': ':blush:', + } + + BOT = { + 'team_id': 'T456DEF', + 'auth_token': 'xoxb-4c5b6a-1f2e3d', + 'channel_id': 'C456DEF', + 'icon_emoji': ':smirk:', + } # ---- Properties ---------------------------------------------------- @@ -123,7 +134,7 @@ def good_add_event(self): 'type': 'emoji_changed', 'value': 'https://gulfcoastmakers.files.wordpress.com/2015/03/blam.jpg', }, - 'team_id': self.TEAM_ID, + 'team_id': self.WORKSPACE['team_id'], 'token': SLACK_VERIFICATION_TOKEN, 'type': 'event_callback', } @@ -142,7 +153,7 @@ def good_remove_event(self): 'subtype': 'remove', 'type': 'emoji_changed', }, - 'team_id': self.TEAM_ID, + 'team_id': self.WORKSPACE['team_id'], 'token': SLACK_VERIFICATION_TOKEN, 'type': 'event_callback', } @@ -160,11 +171,9 @@ def good_url_verification(self): def setUp(self): super().setUp() # type: ignore # py2 - SlackWorkspaceEmojiWatcher( - team_id=self.TEAM_ID, - workspace_token='xoxa-1f2e3d-4c5b6a', - channel_id=self.CLIENT_ID, - ).save() + + for watcher_kw in self.WORKSPACE, self.BOT: + SlackWorkspaceEmojiWatcher(**watcher_kw).save() # ---- Methods ------------------------------------------------------- @@ -276,8 +285,8 @@ def test_event_emoji_bad_team( None, '<...>', 'T' + 'A' * TEAM_ID_MAX_LEN, - list(self.TEAM_ID), - {self.TEAM_ID: None}, + list(self.WORKSPACE['team_id']), + {self.WORKSPACE['team_id']: None}, ): payload_data['team_id'] = team_id res = self.post_event_hook(payload_data) @@ -448,7 +457,12 @@ def test_event_add_emoji( mocked_post_message, # type: mock.MagicMock ): # type: (...) -> None - res = self._post_good_add_event(self.good_add_event, mocked_post_message) + good_add_event = self.good_add_event + res = self._post_good_add_event(good_add_event, mocked_post_message) + self.assertEqual(res.status_code, 200) + + good_add_event['team_id'] = self.BOT['team_id'] + res = self._post_good_add_event(good_add_event, mocked_post_message, self.BOT, as_user=False) self.assertEqual(res.status_code, 200) @mock.patch.object(slacker.Chat, 'post_message') @@ -475,18 +489,23 @@ def _post_good_add_event( self, good_add_event, # type: typing.Dict mocked_post_message, # type: mock.MagicMock + team_dict=None, # type: typing.Optional[typing.Dict] + as_user=None, # type: typing.Optional[bool] ): # type: (...) -> d_http.Response emoji_name = good_add_event['event']['name'] emoji_url = good_add_event['event']['value'] + team_dict = team_dict if team_dict is not None else self.WORKSPACE res = self.post_event_hook(good_add_event) mocked_post_message.assert_called_with( - self.CLIENT_ID, + team_dict['channel_id'], 'added `:{}:`'.format(emoji_name), attachments=[{ 'fallback': '<{}>'.format(emoji_url), 'image_url': emoji_url, - }] + }], + as_user=as_user, + icon_emoji=team_dict['icon_emoji'], ) return res @@ -502,7 +521,12 @@ def test_event_remove_emoji( mocked_post_message, # type: mock.MagicMock ): # type: (...) -> None - res = self._post_good_add_event(self.good_remove_event, mocked_post_message) + good_remove_event = self.good_remove_event + res = self._post_good_remove_event(good_remove_event, mocked_post_message) + self.assertEqual(res.status_code, 200) + + good_remove_event['team_id'] = self.BOT['team_id'] + res = self._post_good_remove_event(good_remove_event, mocked_post_message, self.BOT, as_user=False) self.assertEqual(res.status_code, 200) @mock.patch.object(slacker.Chat, 'post_message') @@ -512,7 +536,7 @@ def test_event_remove_emoji_invalid_auth( ): # type: (...) -> None mocked_post_message.side_effect = slacker.Error('invalid_auth') - res = self._post_good_add_event(self.good_remove_event, mocked_post_message) + res = self._post_good_remove_event(self.good_remove_event, mocked_post_message) self.assertEqual(res.status_code, 403) @mock.patch.object(slacker.Chat, 'post_message') @@ -522,20 +546,25 @@ def test_event_remove_emoji_slacker_errors_ignored( ): # type: (...) -> None mocked_post_message.side_effect = slacker.Error() - res = self._post_good_add_event(self.good_remove_event, mocked_post_message) + res = self._post_good_remove_event(self.good_remove_event, mocked_post_message) self.assertEqual(res.status_code, 200) - def _post_good_add_event( + def _post_good_remove_event( self, good_remove_event, # type: typing.Dict mocked_post_message, # type: mock.MagicMock + team_dict=None, # type: typing.Optional[typing.Dict] + as_user=None, # type: typing.Optional[bool] ): # type: (...) -> d_http.Response emoji_names = good_remove_event['event']['names'] + team_dict = team_dict if team_dict is not None else self.WORKSPACE res = self.post_event_hook(good_remove_event) mocked_post_message.assert_called_with( - self.CLIENT_ID, + team_dict['channel_id'], 'removed `:{}:`'.format(':`, `:'.join(emoji_names)), + as_user=as_user, + icon_emoji=team_dict['icon_emoji'], ) return res