Files
edx-platform/common/djangoapps/track/views/tests/test_segmentio.py
2014-10-09 13:25:20 -04:00

266 lines
11 KiB
Python

"""Ensure we can parse events sent to us from the segment.io webhook integration"""
from datetime import datetime
import json
from ddt import ddt, data
from freezegun import freeze_time
from mock import patch, sentinel
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from track.middleware import TrackMiddleware
from track.views import segmentio
EXPECTED_TIME = datetime(2013, 10, 3, 8, 24, 55)
SECRET = 'anything'
ENDPOINT = '/segmentio/test/event'
USER_ID = 10
@ddt
@override_settings(
TRACKING_SEGMENTIO_WEBHOOK_SECRET=SECRET,
TRACKING_IGNORE_URL_PATTERNS=[ENDPOINT],
TRACKING_SEGMENTIO_ALLOWED_ACTIONS=['Track', 'Screen'],
TRACKING_SEGMENTIO_ALLOWED_CHANNELS=['mobile']
)
@freeze_time(EXPECTED_TIME)
class SegmentIOTrackingTestCase(TestCase):
"""Test processing of segment.io events"""
def setUp(self):
self.request_factory = RequestFactory()
patcher = patch('track.views.segmentio.tracker')
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)
def test_get_request(self):
request = self.request_factory.get(ENDPOINT)
response = segmentio.track_segmentio_event(request)
self.assertEquals(response.status_code, 405)
self.assertFalse(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
@override_settings(
TRACKING_SEGMENTIO_WEBHOOK_SECRET=None
)
def test_no_secret_config(self):
request = self.request_factory.post(ENDPOINT)
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
def assert_segmentio_uncommitted_response(self, response, expected_message, expected_status=400):
"""Assert that no event was emitted and an appropriate commit==false message was returned"""
self.assertEquals(response.status_code, expected_status)
parsed_content = json.loads(response.content)
self.assertEquals(parsed_content, {'committed': False, 'message': expected_message})
self.assertFalse(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
def test_no_secret_provided(self):
request = self.request_factory.post(ENDPOINT)
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
def test_secret_mismatch(self):
request = self.create_request(key='y')
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_UNAUTHORIZED, 401)
def create_request(self, key=None, **kwargs):
"""Create a fake request that emulates a request from the segment.io servers to ours"""
if key is None:
key = SECRET
return self.request_factory.post(ENDPOINT + "?key=" + key, **kwargs)
@data('Identify', 'Group', 'Alias', 'Page', 'identify')
def test_segmentio_ignore_actions(self, action):
response = self.post_segmentio_event(action=action)
self.assert_segmentio_uncommitted_response(response, segmentio.WARNING_IGNORED_ACTION, 200)
def post_segmentio_event(self, **kwargs):
"""Post a fake segment.io event to the view that processes it"""
request = self.create_request(
data=self.create_segmentio_event_json(**kwargs),
content_type='application/json'
)
return segmentio.track_segmentio_event(request)
@data('server', 'browser', 'Browser')
def test_segmentio_ignore_channels(self, channel):
response = self.post_segmentio_event(event_source=channel)
self.assert_segmentio_uncommitted_response(response, segmentio.WARNING_IGNORED_CHANNEL, 200)
def create_segmentio_event(self, **kwargs):
"""Populate a fake segment.io event with data of interest"""
action = kwargs.get('action', 'Track')
sample_event = {
"userId": kwargs.get('user_id', USER_ID),
"event": "Did something",
"properties": {
'event_type': kwargs.get('event_type', ''),
'event_source': kwargs.get('event_source', 'mobile'),
'event': kwargs.get('event', {}),
'context': {
'course_id': kwargs.get('course_id') or '',
},
'name': str(sentinel.name),
},
"channel": kwargs.get('channel', 'mobile'),
"context": {
"library": {
"name": "unknown",
"version": "unknown"
},
'userAgent': str(sentinel.user_agent),
},
"receivedAt": "2014-08-27T16:33:39.100Z",
"timestamp": "2014-08-27T16:33:39.215Z",
"type": action.lower(),
"projectId": "u0j33yjkr8",
"messageId": "qy52hwp4",
"version": 2,
"integrations": {},
"options": {
"library": "unknown",
"providers": {}
},
"action": action
}
return sample_event
def create_segmentio_event_json(self, **kwargs):
"""Return a json string containing a fake segment.io event"""
return json.dumps(self.create_segmentio_event(**kwargs))
def test_no_user_for_user_id(self):
response = self.post_segmentio_event(user_id=40)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_USER_NOT_EXIST, 400)
def test_invalid_user_id(self):
response = self.post_segmentio_event(user_id='foobar')
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_INVALID_USER_ID, 400)
@data('foo/bar/baz', 'course-v1:foo+bar+baz')
def test_success(self, course_id):
middleware = TrackMiddleware()
request = self.create_request(
data=self.create_segmentio_event_json(event_type=str(sentinel.event_type), event={'foo': 'bar'}, course_id=course_id),
content_type='application/json'
)
User.objects.create(pk=USER_ID, username=str(sentinel.username))
middleware.process_request(request)
# The middleware normally emits an event, make sure it doesn't in this case.
self.assertFalse(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
try:
response = segmentio.track_segmentio_event(request)
self.assertEquals(response.status_code, 200)
expected_event = {
'username': str(sentinel.username),
'ip': '',
'event_source': 'mobile',
'event_type': str(sentinel.event_type),
'name': str(sentinel.name),
'event': {'foo': 'bar'},
'agent': str(sentinel.user_agent),
'page': None,
'time': datetime.strptime("2014-08-27T16:33:39.215Z", "%Y-%m-%dT%H:%M:%S.%fZ"),
'host': 'testserver',
'context': {
'user_id': USER_ID,
'course_id': course_id,
'org_id': 'foo',
'path': ENDPOINT,
'client': {
'library': {
'name': 'unknown',
'version': 'unknown'
},
'userAgent': str(sentinel.user_agent)
},
'received_at': datetime.strptime("2014-08-27T16:33:39.100Z", "%Y-%m-%dT%H:%M:%S.%fZ"),
},
}
finally:
middleware.process_response(request, None)
self.mock_tracker.send.assert_called_once_with(expected_event) # pylint: disable=maybe-no-member
def test_invalid_course_id(self):
request = self.create_request(
data=self.create_segmentio_event_json(course_id='invalid'),
content_type='application/json'
)
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = segmentio.track_segmentio_event(request)
self.assertEquals(response.status_code, 200)
self.assertTrue(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
def test_missing_event_type(self):
sample_event_raw = self.create_segmentio_event()
del sample_event_raw['properties']['event_type']
request = self.create_request(
data=json.dumps(sample_event_raw),
content_type='application/json'
)
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_EVENT_TYPE, 400)
def test_missing_timestamp(self):
sample_event_raw = self.create_event_without_fields('timestamp')
request = self.create_request(
data=json.dumps(sample_event_raw),
content_type='application/json'
)
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_TIMESTAMP, 400)
def create_event_without_fields(self, *fields):
"""Create a fake event and remove some fields from it"""
event = self.create_segmentio_event()
for field in fields:
if field in event:
del event[field]
return event
def test_missing_received_at(self):
sample_event_raw = self.create_event_without_fields('receivedAt')
request = self.create_request(
data=json.dumps(sample_event_raw),
content_type='application/json'
)
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = segmentio.track_segmentio_event(request)
self.assert_segmentio_uncommitted_response(response, segmentio.ERROR_MISSING_RECEIVED_AT, 400)
def test_string_user_id(self):
User.objects.create(pk=USER_ID, username=str(sentinel.username))
response = self.post_segmentio_event(user_id=str(USER_ID))
result = self.assert_segmentio_committed_response(response)
self.assertEquals(result['context']['user_id'], USER_ID)
def assert_segmentio_committed_response(self, response):
"""Assert that an event was emitted"""
self.assertEquals(response.status_code, 200)
parsed_content = json.loads(response.content)
self.assertEquals(parsed_content, {'committed': True})
self.assertTrue(self.mock_tracker.send.called) # pylint: disable=maybe-no-member
return self.mock_tracker.send.mock_calls[0][1][0]