diff --git a/MANIFEST.in b/MANIFEST.in index f89dcd3..6360e94 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,3 +14,4 @@ include \ tests/requirements.txt recursive-include emojiwatch/static * +recursive-include emojiwatch/templates * diff --git a/docs/intro.rst b/docs/intro.rst index a6d3914..6ce2ff4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -65,6 +65,7 @@ Now you can add it to your ``DJANGO_SETTINGS_MODULE``: ) EMOJIWATCH = { + 'slack_auth_token': '...', 'slack_verification_token': '...', } @@ -88,34 +89,13 @@ If you haven't already, you'll also need to `enable the admin site `__ for details. -Slack App Setup -~~~~~~~~~~~~~~~ +Slack App and Watcher Setup +~~~~~~~~~~~~~~~~~~~~~~~~~~~ For illustration, we'll create a `workspace-based Slack app `__, but we could just as easily use a traditional one. diff --git a/emojiwatch/__init__.py b/emojiwatch/__init__.py index 9de3569..5ab7eea 100644 --- a/emojiwatch/__init__.py +++ b/emojiwatch/__init__.py @@ -36,8 +36,7 @@ LOGGER = _logging.getLogger(__name__) SETTINGS = getattr(d_conf.settings, 'EMOJIWATCH', {}) +SLACK_AUTH_TOKEN = SETTINGS.get('slack_auth_token') +SLACK_VERIFICATION_TOKEN = SETTINGS.get('slack_verification_token') -try: - SLACK_VERIFICATION_TOKEN = SETTINGS['slack_verification_token'] -except (KeyError, TypeError): - SLACK_VERIFICATION_TOKEN = None +default_app_config = 'emojiwatch.apps.EmojiwatchConfig' diff --git a/emojiwatch/apps.py b/emojiwatch/apps.py new file mode 100644 index 0000000..c6d1e10 --- /dev/null +++ b/emojiwatch/apps.py @@ -0,0 +1,57 @@ +# -*- encoding: utf-8 -*- +# ====================================================================== +""" +Copyright and other protections apply. Please see the accompanying +:doc:`LICENSE ` and :doc:`CREDITS ` file(s) for rights +and restrictions governing use of this software. All rights not +expressly waived or licensed are reserved. If those files are missing or +appear to be modified from their originals, then please contact the +author before viewing or using this software in any capacity. +""" +# ====================================================================== + +from __future__ import absolute_import, division, print_function + +TYPE_CHECKING = False # from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression + +from builtins import * # noqa: F401,F403 # pylint: disable=redefined-builtin,unused-wildcard-import,useless-suppression,wildcard-import +from future.builtins.disabled import * # noqa: F401,F403 # pylint: disable=no-name-in-module,redefined-builtin,unused-wildcard-import,useless-suppression,wildcard-import +from future.standard_library import install_aliases +install_aliases() + +# ---- Imports --------------------------------------------------------- + +from gettext import gettext + +import django.apps as d_apps + +from . import ( + LOGGER, + SLACK_AUTH_TOKEN, + SLACK_VERIFICATION_TOKEN, +) + +# ---- Classes --------------------------------------------------------- + +# ====================================================================== +class EmojiwatchConfig(d_apps.AppConfig): + + # ---- Data -------------------------------------------------------- + + name = 'emojiwatch' + verbose_name = gettext('Emojiwatch') + + # ---- Overrides --------------------------------------------------- + + def ready(self): + # type: (...) -> None + super().ready() # type: ignore # py2 + + if not SLACK_AUTH_TOKEN: + LOGGER.critical("EMOJIWATCH['slack_auth_token'] setting is missing") + + if not SLACK_VERIFICATION_TOKEN: + LOGGER.critical("EMOJIWATCH['slack_verification_token'] setting is missing") diff --git a/emojiwatch/migrations/0001_initial.py b/emojiwatch/migrations/0001_initial.py index 63dede7..2955063 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 20:56 import django.core.validators from django.db import migrations, models @@ -18,9 +18,9 @@ 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')), + ('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..0854391 100644 --- a/emojiwatch/models.py +++ b/emojiwatch/models.py @@ -24,7 +24,6 @@ # ---- Imports ----------------------------------------------------------- import fernet_fields -import slacker from gettext import gettext @@ -36,11 +35,12 @@ __all__ = () 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 +96,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 +116,7 @@ class Meta(object): # ---- Properties ---------------------------------------------------- team_id = d_d_models.CharField( + default='T', max_length=TEAM_ID_MAX_LEN, null=False, unique=True, @@ -127,30 +128,32 @@ class Meta(object): team_id.short_description = gettext('Team ID (e.g., T123ABC...)') - workspace_token = fernet_fields.EncryptedCharField( - max_length=WORKSPACE_TOKEN_MAX_LEN, + channel_id = d_d_models.CharField( + default='C', + max_length=CHANNEL_ID_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(CHANNEL_ID_RE, message=gettext('Must be of the format (e.g.) C123ABC...')), ], - verbose_name=gettext('Workspace Token'), + verbose_name=gettext('Channel ID'), ) - channel_id = d_d_models.CharField( - max_length=CHANNEL_ID_MAX_LEN, + channel_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(CHANNEL_ID_RE, message=gettext('Must be of the format (e.g.) C123ABC...')), + d_c_validators.RegexValidator(ICON_EMOJI_RE, message=gettext('Must be of the format (e.g.) :emoji_name:...')), ], - verbose_name=gettext('Channel ID'), + 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='', verbose_name=gettext('Notes'), ) - - @property - def slack(self): - return slacker.Slacker(self.workspace_token) diff --git a/emojiwatch/templates/admin/emojiwatch/change_form.html b/emojiwatch/templates/admin/emojiwatch/change_form.html new file mode 100644 index 0000000..048e661 --- /dev/null +++ b/emojiwatch/templates/admin/emojiwatch/change_form.html @@ -0,0 +1,5 @@ +{% extends "admin/change_form.html" %} +{% load i18n %} +{% block after_field_sets %}

+{% trans 'A watcher for receiving emoji_changed events for a given team and posting updates to a given channel via chat.postMessage.' %} +

{% endblock %} diff --git a/emojiwatch/views.py b/emojiwatch/views.py index 9e4f5e5..211e996 100644 --- a/emojiwatch/views.py +++ b/emojiwatch/views.py @@ -34,8 +34,11 @@ 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_AUTH_TOKEN, SLACK_VERIFICATION_TOKEN, ) from .models import ( @@ -53,7 +56,8 @@ _EMOJI_URL_MAX_LEN = 1023 _EMOJIS_MAX_LEN = 32 _FIELD_MAX_LEN = 63 -_SHRUG = '\u00af\\_(\u30c4)_/\u00af' +_SHRUG = str(u'\u00af\\_(\u30c4)_/\u00af') +_SLACK = slacker.Slacker(SLACK_AUTH_TOKEN) _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 +75,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 @@ -112,6 +116,11 @@ def dispatch(self, request, *args, **kw): def event_hook_handler( request, # type: d_http.HttpRequest ): # type: (...) -> d_http.HttpResponse + if not SLACK_AUTH_TOKEN: + LOGGER.critical("EMOJIWATCH['slack_auth_token'] setting is missing") + + return d_http.HttpResponseServerError() + if not SLACK_VERIFICATION_TOKEN: LOGGER.critical("EMOJIWATCH['slack_verification_token'] setting is missing") @@ -121,7 +130,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 +141,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 +157,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 +170,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 +178,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 +206,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 +217,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 +240,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 +259,19 @@ def event_hook_handler( return d_http.HttpResponse() +# ======================================================================== +def _as_user(): + # 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-', SLACK_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 +284,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 +299,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( + return _SLACK.chat.post_message( team.channel_id, - html.escape('added `:{}:`').format(emoji_name), + html.escape(gettext('added `:{}:`').format(emoji_name)), + as_user=_as_user(), attachments=attachments, + icon_emoji=_icon_emoji(team), ) # ======================================================================== @@ -296,7 +320,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 +332,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( + return _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(), + icon_emoji=_icon_emoji(team), ) # ---- Initialization ---------------------------------------------------- diff --git a/tests/django_settings.py b/tests/django_settings.py index ba6625b..53208bb 100644 --- a/tests/django_settings.py +++ b/tests/django_settings.py @@ -45,7 +45,8 @@ DEBUG = True EMOJIWATCH = { - 'slack_verification_token': 'FoRtEsTiNgOnLyNoTrEaLlYaVeRiFiCaTiOnToKeN', + 'slack_auth_token': 'xoxa-for-testing-only-not-a-real-auth-token', + 'slack_verification_token': 'FoRtEsTiNgOnLyNoTaReAlVeRiFiCaTiOnToKeN', } INSTALLED_APPS = ( diff --git a/tests/test_models.py b/tests/test_models.py index 9cf6457..0653382 100755 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -59,22 +59,33 @@ def test_form_validation(self): for field in ( 'team_id', - 'workspace_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.channel_id = '' + + with self.assertRaises(d_c_exceptions.ValidationError) as cm: + ws_emoji_watcher.full_clean() + + for field in ( + 'team_id', '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.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'), ('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 +93,8 @@ 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.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 +107,8 @@ 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.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 +122,14 @@ 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.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.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 +141,8 @@ 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.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..0c89a9d 100755 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -26,6 +26,7 @@ import json import slacker +import re import unittest try: @@ -40,7 +41,12 @@ import tests tests.setup() -from emojiwatch import SLACK_VERIFICATION_TOKEN +import emojiwatch.views + +from emojiwatch import ( + SLACK_AUTH_TOKEN, + SLACK_VERIFICATION_TOKEN, +) from emojiwatch.models import ( SlackWorkspaceEmojiWatcher, TEAM_ID_MAX_LEN, @@ -108,8 +114,13 @@ class EventHandlerTestCaseBase(d_test.TestCase): # ---- Data ---------------------------------------------------------- - CLIENT_ID = 'C123ABC' - TEAM_ID = 'T123ABC' + WORKSPACE = { + 'team_id': 'T123ABC', + 'channel_id': 'C123ABC', + 'icon_emoji': ':blush:', + } + + BOT_AUTH_TOKEN = re.sub(r'\Axox.-', 'xoxb-', SLACK_AUTH_TOKEN) # ---- Properties ---------------------------------------------------- @@ -123,7 +134,8 @@ 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 +154,8 @@ 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 +173,7 @@ 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() + SlackWorkspaceEmojiWatcher(**self.WORKSPACE).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,9 +457,14 @@ 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) + with mock.patch.object(emojiwatch.views, 'SLACK_AUTH_TOKEN', self.BOT_AUTH_TOKEN): + res = self._post_good_add_event(good_add_event, mocked_post_message, as_user=False) + self.assertEqual(res.status_code, 200) + @mock.patch.object(slacker.Chat, 'post_message') def test_event_add_emoji_invalid_auth( self, @@ -475,18 +489,21 @@ def _post_good_add_event( self, good_add_event, # type: typing.Dict mocked_post_message, # type: mock.MagicMock + 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'] res = self.post_event_hook(good_add_event) mocked_post_message.assert_called_with( - self.CLIENT_ID, + self.WORKSPACE['channel_id'], 'added `:{}:`'.format(emoji_name), attachments=[{ 'fallback': '<{}>'.format(emoji_url), 'image_url': emoji_url, - }] + }], + as_user=as_user, + icon_emoji=self.WORKSPACE['icon_emoji'], ) return res @@ -502,9 +519,14 @@ 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) + with mock.patch.object(emojiwatch.views, 'SLACK_AUTH_TOKEN', self.BOT_AUTH_TOKEN): + res = self._post_good_remove_event(good_remove_event, mocked_post_message, as_user=False) + self.assertEqual(res.status_code, 200) + @mock.patch.object(slacker.Chat, 'post_message') def test_event_remove_emoji_invalid_auth( self, @@ -512,7 +534,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 +544,23 @@ 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 + as_user=None, # type: typing.Optional[bool] ): # type: (...) -> d_http.Response emoji_names = good_remove_event['event']['names'] res = self.post_event_hook(good_remove_event) mocked_post_message.assert_called_with( - self.CLIENT_ID, + self.WORKSPACE['channel_id'], 'removed `:{}:`'.format(':`, `:'.join(emoji_names)), + as_user=as_user, + icon_emoji=self.WORKSPACE['icon_emoji'], ) return res