diff --git a/pillar/application/modules/encoding.py b/pillar/application/modules/encoding.py index 02a688c3..af815f32 100644 --- a/pillar/application/modules/encoding.py +++ b/pillar/application/modules/encoding.py @@ -1,58 +1,65 @@ +import logging + from bson import ObjectId from eve.methods.put import put_internal from flask import Blueprint from flask import abort from flask import request from application import app +from application import utils encoding = Blueprint('encoding', __name__) +log = logging.getLogger(__name__) @encoding.route('/zencoder/notifications', methods=['POST']) def zencoder_notifications(): - if app.config['ENCODING_BACKEND'] == 'zencoder': - if not app.config['DEBUG']: - # If we are in production, look for the Zencoder header secret - try: - notification_secret_request = request.headers[ - 'X-Zencoder-Notification-Secret'] - except KeyError: - return abort(401) - # If the header is found, check it agains the one in the config - notification_secret = app.config['ZENCODER_NOTIFICATIONS_SECRET'] - if notification_secret_request != notification_secret: - return abort(401) - # Cast request data into a dict - data = request.get_json() - files_collection = app.data.driver.db['files'] - # Find the file object based on processing backend and job_id - lookup = {'processing.backend': 'zencoder', 'processing.job_id': str( - data['job']['id'])} - f = files_collection.find_one(lookup) - if f: - file_id = f['_id'] - # Remove internal keys (so that we can run put internal) - internal_fields = ['_id', '_etag', '_updated', '_created', '_status'] - for field in internal_fields: - f.pop(field, None) - # Update processing status - f['processing']['status'] = data['job']['state'] - # For every variation encoded, try to update the file object - for output in data['outputs']: - format = output['format'] - # Change the zencoder 'mpeg4' format to 'mp4' used internally - format = 'mp4' if format == 'mpeg4' else format - # Find a variation matching format and resolution - variation = next((v for v in f['variations'] if v['format'] == format \ - and v['width'] == output['width']), None) - # If found, update with delivered file size - # TODO: calculate md5 on the storage - if variation: - variation['length'] = output['file_size_in_bytes'] - - r = put_internal('files', f, **{'_id': ObjectId(file_id)}) - return '' - else: - return abort(404) - else: + if app.config['ENCODING_BACKEND'] != 'zencoder': + log.warning('Received notification from Zencoder but app not configured for Zencoder.') return abort(403) + + if not app.config['DEBUG']: + # If we are in production, look for the Zencoder header secret + try: + notification_secret_request = request.headers[ + 'X-Zencoder-Notification-Secret'] + except KeyError: + log.warning('Received Zencoder notification without secret.') + return abort(401) + # If the header is found, check it agains the one in the config + notification_secret = app.config['ZENCODER_NOTIFICATIONS_SECRET'] + if notification_secret_request != notification_secret: + log.warning('Received Zencoder notification with incorrect secret.') + return abort(401) + + # Cast request data into a dict + data = request.get_json() + files_collection = app.data.driver.db['files'] + # Find the file object based on processing backend and job_id + lookup = {'processing.backend': 'zencoder', 'processing.job_id': str(data['job']['id'])} + f = files_collection.find_one(lookup) + if not f: + log.warning('Unknown Zencoder job id %r', data['job']['id']) + return abort(404) + + file_id = f['_id'] + # Remove internal keys (so that we can run put internal) + f = utils.remove_private_keys(f) + + # Update processing status + f['processing']['status'] = data['job']['state'] + # For every variation encoded, try to update the file object + for output in data['outputs']: + format = output['format'] + # Change the zencoder 'mpeg4' format to 'mp4' used internally + format = 'mp4' if format == 'mpeg4' else format + # Find a variation matching format and resolution + variation = next((v for v in f['variations'] if v['format'] == format \ + and v['width'] == output['width']), None) + # If found, update with delivered file size + # TODO: calculate md5 on the storage + if variation: + variation['length'] = output['file_size_in_bytes'] + + put_internal('files', f, _id=ObjectId(file_id)) + return '' diff --git a/tests/test_encoding.py b/tests/test_encoding.py new file mode 100644 index 00000000..18e8a843 --- /dev/null +++ b/tests/test_encoding.py @@ -0,0 +1,50 @@ +"""Test cases for the zencoder notifications.""" +import json + +from common_test_class import AbstractPillarTest + + +class ZencoderNotificationTest(AbstractPillarTest): + + def test_missing_secret(self): + with self.app.test_request_context(): + resp = self.client.post('/encoding/zencoder/notifications') + self.assertEqual(401, resp.status_code) + + def test_wrong_secret(self): + with self.app.test_request_context(): + resp = self.client.post('/encoding/zencoder/notifications', + headers={'X-Zencoder-Notification-Secret': 'koro'}) + self.assertEqual(401, resp.status_code) + + def test_good_secret_missing_file(self): + with self.app.test_request_context(): + secret = self.app.config['ZENCODER_NOTIFICATIONS_SECRET'] + resp = self.client.post('/encoding/zencoder/notifications', + data=json.dumps({'job': {'id': 'koro-007'}}), + headers={'X-Zencoder-Notification-Secret': secret, + 'Content-Type': 'application/json'}) + self.assertEqual(404, resp.status_code) + + def test_good_secret_existing_file(self): + self.ensure_file_exists(file_overrides={ + 'processing': {'backend': 'zencoder', + 'job_id': 'koro-007', + 'status': 'processing'} + }) + + with self.app.test_request_context(): + secret = self.app.config['ZENCODER_NOTIFICATIONS_SECRET'] + resp = self.client.post('/encoding/zencoder/notifications', + data=json.dumps({'job': {'id': 'koro-007', + 'state': 'done'}, + 'outputs': [{ + 'format': 'jpg', + 'width': 2048, + 'file_size_in_bytes': 15, + }]}), + headers={'X-Zencoder-Notification-Secret': secret, + 'Content-Type': 'application/json'}) + + # TODO: check that the file in MongoDB is actually updated properly. + self.assertEqual(200, resp.status_code)