diff --git a/tests/common_test_stuff.py b/tests/common_test_stuff.py new file mode 100644 index 00000000..b1950883 --- /dev/null +++ b/tests/common_test_stuff.py @@ -0,0 +1,82 @@ +import json +import copy +import sys +import logging +import os + +from eve.tests import TestMinimal +import pymongo.collection +from flask.testing import FlaskClient +import httpretty + +from test_data import EXAMPLE_PROJECT, EXAMPLE_FILE + +BLENDER_ID_ENDPOINT = 'http://127.0.0.1:8001' # nonexistant server, no trailing slash! +MY_PATH = os.path.dirname(os.path.abspath(__file__)) + +TEST_EMAIL_USER = 'koro' +TEST_EMAIL_ADDRESS = '%s@testing.blender.org' % TEST_EMAIL_USER + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)-15s %(levelname)8s %(name)s %(message)s') + + +class AbstractPillarTest(TestMinimal): + def setUp(self, **kwargs): + settings_file = os.path.join(MY_PATH, 'test_settings.py') + kwargs['settings_file'] = settings_file + os.environ['EVE_SETTINGS'] = settings_file + super(AbstractPillarTest, self).setUp(**kwargs) + + from application import app + + app.config['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT + logging.getLogger('application').setLevel(logging.DEBUG) + logging.getLogger('werkzeug').setLevel(logging.DEBUG) + + self.app = app + self.client = app.test_client() + assert isinstance(self.client, FlaskClient) + + def tearDown(self): + super(AbstractPillarTest, self).tearDown() + + # Not only delete self.app (like the superclass does), + # but also un-import the application. + del sys.modules['application'] + + def _ensure_file_exists(self, file_overrides=None): + with self.app.test_request_context(): + files_collection = self.app.data.driver.db['files'] + projects_collection = self.app.data.driver.db['projects'] + assert isinstance(files_collection, pymongo.collection.Collection) + + file = copy.deepcopy(EXAMPLE_FILE) + if file_overrides is not None: + file.update(file_overrides) + + projects_collection.insert_one(EXAMPLE_PROJECT) + result = files_collection.insert_one(file) + file_id = result.inserted_id + return file_id, EXAMPLE_FILE + + def htp_blenderid_validate_unhappy(self): + """Sets up HTTPretty to mock unhappy validation flow.""" + + httpretty.register_uri(httpretty.POST, + '%s/u/validate_token' % BLENDER_ID_ENDPOINT, + body=json.dumps( + {'data': {'token': 'Token is invalid'}, 'status': 'fail'}), + content_type="application/json") + + def htp_blenderid_validate_happy(self): + """Sets up HTTPretty to mock happy validation flow.""" + + httpretty.register_uri(httpretty.POST, + '%s/u/validate_token' % BLENDER_ID_ENDPOINT, + body=json.dumps( + {'data': {'user': {'email': TEST_EMAIL_ADDRESS, 'id': 5123}}, + 'status': 'success'}), + content_type="application/json") + diff --git a/tests/test_auth.py b/tests/test_auth.py index 26c14812..392ee824 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,23 +1,7 @@ -import unittest -import os import base64 import httpretty -import json -BLENDER_ID_ENDPOINT = 'http://127.0.0.1:8001' # nonexistant server, no trailing slash! -TEST_EMAIL_USER = 'koro' -TEST_EMAIL_ADDRESS = '%s@testing.blender.org' % TEST_EMAIL_USER - -os.environ['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT -os.environ['MONGO_DBNAME'] = 'unittest' -os.environ['EVE_SETTINGS'] = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - 'pillar', 'settings.py') - -from application import app -from application.utils import authentication as auth - -app.config['BLENDER_ID_ENDPOINT'] = BLENDER_ID_ENDPOINT +from common_test_stuff import AbstractPillarTest, TEST_EMAIL_USER, TEST_EMAIL_ADDRESS def make_header(username, password=''): @@ -26,19 +10,11 @@ def make_header(username, password=''): return 'basic ' + base64.b64encode('%s:%s' % (username, password)) -class AuthenticationTests(unittest.TestCase): - def setUp(self): - self.app = app.test_client() - with app.test_request_context(): - self.delete_test_data() - - def tearDown(self): - with app.test_request_context(): - self.delete_test_data() - +class AuthenticationTests(AbstractPillarTest): def test_make_unique_username(self): + from application.utils import authentication as auth - with app.test_request_context(): + with self.app.test_request_context(): # This user shouldn't exist yet. self.assertEqual(TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS)) @@ -46,44 +22,29 @@ class AuthenticationTests(unittest.TestCase): auth.create_new_user(TEST_EMAIL_ADDRESS, TEST_EMAIL_USER, 'test1234') self.assertEqual('%s1' % TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS)) - def delete_test_data(self): - app.data.driver.db.drop_collection('users') - app.data.driver.db.drop_collection('tokens') - - def blenderid_validate_unhappy(self): - """Sets up HTTPretty to mock unhappy validation flow.""" - - httpretty.register_uri(httpretty.POST, - '%s/u/validate_token' % BLENDER_ID_ENDPOINT, - body=json.dumps({'data': {'token': 'Token is invalid'}, 'status': 'fail'}), - content_type="application/json") - - def blenderid_validate_happy(self): - """Sets up HTTPretty to mock happy validation flow.""" - - httpretty.register_uri(httpretty.POST, - '%s/u/validate_token' % BLENDER_ID_ENDPOINT, - body=json.dumps({'data': {'user': {'email': TEST_EMAIL_ADDRESS, 'id': 5123}}, - 'status': 'success'}), - content_type="application/json") - @httpretty.activate def test_validate_token__not_logged_in(self): - with app.test_request_context(): + from application.utils import authentication as auth + + with self.app.test_request_context(): self.assertFalse(auth.validate_token()) @httpretty.activate def test_validate_token__unknown_token(self): """Test validating of invalid token, unknown both to us and Blender ID.""" - self.blenderid_validate_unhappy() - with app.test_request_context(headers={'Authorization': make_header('unknowntoken')}): + from application.utils import authentication as auth + + self.htp_blenderid_validate_unhappy() + with self.app.test_request_context(headers={'Authorization': make_header('unknowntoken')}): self.assertFalse(auth.validate_token()) @httpretty.activate def test_validate_token__unknown_but_valid_token(self): """Test validating of valid token, unknown to us but known to Blender ID.""" - self.blenderid_validate_happy() - with app.test_request_context(headers={'Authorization': make_header('knowntoken')}): + from application.utils import authentication as auth + + self.htp_blenderid_validate_happy() + with self.app.test_request_context(headers={'Authorization': make_header('knowntoken')}): self.assertTrue(auth.validate_token()) diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 00000000..27424ae1 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,282 @@ +import datetime + +from bson import tz_util, ObjectId + + +EXAMPLE_FILE = {u'_id': ObjectId('5672e2c1c379cf0007b31995'), + u'_updated': datetime.datetime(2016, 3, 25, 10, 28, 24, tzinfo=tz_util.utc), + u'height': 2048, + u'name': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', u'format': 'png', + u'variations': [ + {u'format': 'jpg', u'height': 160, u'width': 160, u'length': 8558, + u'link': 'http://localhost:8002/file-variant-h', u'content_type': 'image/jpeg', + u'md5': '--', u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-b.jpg', + u'size': 'b'}, + {u'format': 'jpg', u'height': 2048, u'width': 2048, u'length': 819569, + u'link': 'http://localhost:8002/file-variant-h', u'content_type': 'image/jpeg', + u'md5': '--', u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4-h.jpg', + u'size': 'h'}, ], + u'filename': 'brick_dutch_soft_bump.png', + u'project': ObjectId('5672beecc0261b2005ed1a33'), + u'width': 2048, u'length': 6227670, u'user': ObjectId('56264fc4fa3a250344bd10c5'), + u'content_type': 'image/png', u'_etag': '044ce3aede2e123e261c0d8bd77212f264d4f7b0', + u'_created': datetime.datetime(2015, 12, 17, 16, 28, 49, tzinfo=tz_util.utc), + u'md5': '', + u'file_path': 'c2a5c897769ce1ef0eb10f8fa1c472bcb8e2d5a4.png', u'backend': 'gcs', + u'link': 'http://localhost:8002/file', + u'link_expires': datetime.datetime(2016, 3, 22, 9, 28, 22, tzinfo=tz_util.utc)} + + +EXAMPLE_PROJECT = { + u'_created': datetime.datetime(2015, 12, 17, 13, 22, 56, tzinfo=tz_util.utc), + u'_etag': u'cc4643e98d3606f87bbfaaa200bfbae941b642f3', + u'_id': ObjectId('5672beecc0261b2005ed1a33'), + u'_updated': datetime.datetime(2016, 1, 7, 18, 59, 4, tzinfo=tz_util.utc), + u'category': u'assets', + u'description': u'Welcome to this curated collection of Blender Institute textures and image resources. This collection is an on-going project, as with each project we create a number of textures based on our own resources (photographs, scans, etc.) or made completely from scratch. At the moment you can find all the textures from the past Open Projects that were deemed re-usable. \r\n\r\nPeople who have contributed to these textures:\r\n\r\nAndrea Weikert, Andy Goralczyk, Basse Salmela, Ben Dansie, Campbell Barton, Enrico Valenza, Ian Hubert, Kjartan Tysdal, Manu J\xe4rvinen, Massimiliana Pulieso, Matt Ebb, Pablo Vazquez, Rob Tuytel, Roland Hess, Sarah Feldlaufer, S\xf6nke M\xe4ter', + u'is_private': False, + u'name': u'Textures', + u'node_types': [{u'description': u'Group for texture node type', + u'dyn_schema': {u'order': {u'type': u'integer'}, + u'status': {u'allowed': [u'published', u'pending', u'deleted'], + u'type': u'string'}, + u'url': {u'type': u'string'}}, + u'form_schema': {u'order': {}, u'status': {}, u'url': {}}, + u'name': u'group_texture', + u'parent': [u'group_texture', u'project'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET']}, + {u'group': ObjectId('564c52b96dcaf85da2faef00'), + u'methods': [u'GET', u'POST']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'Generic group node type edited', + u'dyn_schema': {u'notes': {u'maxlength': 256, u'type': u'string'}, + u'order': {u'type': u'integer'}, + u'status': {u'allowed': [u'published', u'pending', u'deleted'], + u'type': u'string'}, + u'url': {u'type': u'string'}}, + u'form_schema': {u'notes': {}, u'order': {}, u'status': {}, u'url': {}}, + u'name': u'group', + u'parent': [u'group', u'project'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'Basic Asset Type', + u'dyn_schema': { + u'attachments': {u'schema': {u'schema': {u'field': {u'type': u'string'}, + u'files': {u'schema': { + u'schema': {u'file': { + u'data_relation': { + u'embeddable': True, + u'field': u'_id', + u'resource': u'files'}, + u'type': u'objectid'}, + u'size': { + u'type': u'string'}, + u'slug': { + u'minlength': 1, + u'type': u'string'}}, + u'type': u'dict'}, + u'type': u'list'}}, + u'type': u'dict'}, + u'type': u'list'}, + u'categories': {u'type': u'string'}, + u'content_type': {u'type': u'string'}, + u'file': {u'data_relation': {u'embeddable': True, + u'field': u'_id', + u'resource': u'files'}, + u'type': u'objectid'}, + u'order': {u'type': u'integer'}, + u'status': {u'allowed': [u'published', + u'pending', + u'processing', + u'deleted'], + u'type': u'string'}, + u'tags': {u'schema': {u'type': u'string'}, u'type': u'list'}}, + u'form_schema': {u'attachments': {u'visible': False}, + u'categories': {}, + u'content_type': {u'visible': False}, + u'file': {u'visible': False}, + u'order': {}, + u'status': {}, + u'tags': {}}, + u'name': u'asset', + u'parent': [u'group'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'Entrypoint to a remote or local storage solution', + u'dyn_schema': {u'backend': {u'type': u'string'}, + u'subdir': {u'type': u'string'}}, + u'form_schema': {u'backend': {}, u'subdir': {}}, + u'name': u'storage', + u'parent': [u'group', u'project'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET']}], + u'users': [], + u'world': []}}, + {u'description': u'Comments for asset nodes, pages, etc.', + u'dyn_schema': {u'confidence': {u'type': u'float'}, + u'content': {u'minlength': 5, u'type': u'string'}, + u'is_reply': {u'type': u'boolean'}, + u'rating_negative': {u'type': u'integer'}, + u'rating_positive': {u'type': u'integer'}, + u'ratings': {u'schema': { + u'schema': {u'is_positive': {u'type': u'boolean'}, + u'user': {u'type': u'objectid'}, + u'weight': {u'type': u'integer'}}, + u'type': u'dict'}, + u'type': u'list'}, + u'status': {u'allowed': [u'published', u'deleted', u'flagged', + u'edited'], + u'type': u'string'}}, + u'form_schema': {u'confidence': {}, + u'content': {}, + u'is_reply': {}, + u'rating_negative': {}, + u'rating_positive': {}, + u'ratings': {}, + u'status': {}}, + u'name': u'comment', + u'parent': [u'asset', u'comment'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET', u'POST']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET', u'POST']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'Container for node_type post.', + u'dyn_schema': {u'categories': {u'schema': {u'type': u'string'}, + u'type': u'list'}, + u'template': {u'type': u'string'}}, + u'form_schema': {u'categories': {}, u'template': {}}, + u'name': u'blog', + u'parent': [u'project'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'A blog post, for any project', + u'dyn_schema': { + u'attachments': {u'schema': {u'schema': {u'field': {u'type': u'string'}, + u'files': {u'schema': { + u'schema': {u'file': { + u'data_relation': { + u'embeddable': True, + u'field': u'_id', + u'resource': u'files'}, + u'type': u'objectid'}, + u'size': { + u'type': u'string'}, + u'slug': { + u'minlength': 1, + u'type': u'string'}}, + u'type': u'dict'}, + u'type': u'list'}}, + u'type': u'dict'}, + u'type': u'list'}, + u'category': {u'type': u'string'}, + u'content': {u'maxlength': 90000, + u'minlength': 5, + u'required': True, + u'type': u'string'}, + u'status': {u'allowed': [u'published', u'deleted', u'pending'], + u'default': u'pending', + u'type': u'string'}, + u'url': {u'type': u'string'}}, + u'form_schema': {u'attachments': {u'visible': False}, + u'category': {}, + u'content': {}, + u'status': {}, + u'url': {}}, + u'name': u'post', + u'parent': [u'blog'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}], + u'users': [], + u'world': [u'GET']}}, + {u'description': u'Image Texture', + u'dyn_schema': {u'aspect_ratio': {u'type': u'float'}, + u'categories': {u'type': u'string'}, + u'files': {u'schema': {u'schema': { + u'file': {u'data_relation': {u'embeddable': True, + u'field': u'_id', + u'resource': u'files'}, + u'type': u'objectid'}, + u'is_tileable': {u'type': u'boolean'}, + u'map_type': {u'allowed': [u'spec', + u'bump', + u'nor', + u'col', + u'translucency', + u'emission', + u'alpha'], + u'type': u'string'}}, + u'type': u'dict'}, + u'type': u'list'}, + u'is_landscape': {u'type': u'boolean'}, + u'is_tileable': {u'type': u'boolean'}, + u'order': {u'type': u'integer'}, + u'resolution': {u'type': u'string'}, + u'status': {u'allowed': [u'published', + u'pending', + u'processing', + u'deleted'], + u'type': u'string'}, + u'tags': {u'schema': {u'type': u'string'}, u'type': u'list'}}, + u'form_schema': {u'aspect_ratio': {}, + u'categories': {}, + u'content_type': {u'visible': False}, + u'files': {u'visible': False}, + u'is_landscape': {}, + u'is_tileable': {}, + u'order': {}, + u'resolution': {}, + u'status': {}, + u'tags': {}}, + u'name': u'texture', + u'parent': [u'group'], + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}, + {u'group': ObjectId('5596e975ea893b269af85c0f'), + u'methods': [u'GET']}, + {u'group': ObjectId('564733b56dcaf85da2faee8a'), + u'methods': [u'GET']}], + u'users': [], + u'world': [u'GET']}}], + u'nodes_blog': [], + u'nodes_featured': [], + u'nodes_latest': [], + u'organization': ObjectId('55a99fb43004867fb9934f01'), + u'owners': {u'groups': [], u'users': []}, + u'permissions': {u'groups': [{u'group': ObjectId('5596e975ea893b269af85c0e'), + u'methods': [u'GET', u'PUT', u'POST']}], + u'users': [], + u'world': [u'GET']}, + u'picture_header': ObjectId('5673f260c379cf0007b31bc4'), + u'picture_square': ObjectId('5673f256c379cf0007b31bc3'), + u'status': u'published', + u'summary': u'Texture collection from all Blender Institute open projects.', + u'url': u'textures', + u'user': ObjectId('552b066b41acdf5dec4436f2')} \ No newline at end of file diff --git a/tests/test_file_caching.py b/tests/test_file_caching.py new file mode 100644 index 00000000..da0c0f0c --- /dev/null +++ b/tests/test_file_caching.py @@ -0,0 +1,58 @@ +"""Test cases for the /files/{id} entrypoint, testing cache behaviour.""" + +import bson.tz_util +import datetime +from eve import RFC1123_DATE_FORMAT + +from common_test_stuff import AbstractPillarTest + + +class FileCachingTest(AbstractPillarTest): + + def test_nonexistant_file(self): + with self.app.test_request_context(): + resp = self.client.get('/files/12345') + self.assertEqual(404, resp.status_code) + + def test_existing_file(self): + file_id, _ = self._ensure_file_exists() + + resp = self.client.get('/files/%s' % file_id) + self.assertEqual(200, resp.status_code) + + def test_if_modified_304(self): + with self.app.test_request_context(): + # Make sure the file link has not expired. + expires = datetime.datetime.now(tz=bson.tz_util.utc) + datetime.timedelta(minutes=1) + file_id, file_doc = self._ensure_file_exists(file_overrides={ + u'link_expires': expires + }) + + updated = file_doc['_updated'].strftime(RFC1123_DATE_FORMAT) + resp = self.client.get('/files/%s' % file_id, + headers={'if_modified_since': updated}) + self.assertEqual(304, resp.status_code) + + def test_if_modified_200(self): + file_id, file_doc = self._ensure_file_exists() + + delta = datetime.timedelta(days=-1) + + with self.app.test_request_context(): + updated = (file_doc['_updated'] + delta).strftime(RFC1123_DATE_FORMAT) + resp = self.client.get('/files/%s' % file_id, + headers={'if_modified_since': updated}) + self.assertEqual(200, resp.status_code) + + def test_if_modified_link_expired(self): + with self.app.test_request_context(): + # Make sure the file link has expired. + expires = datetime.datetime.now(tz=bson.tz_util.utc) - datetime.timedelta(seconds=1) + file_id, file_doc = self._ensure_file_exists(file_overrides={ + u'link_expires': expires + }) + + updated = file_doc['_updated'].strftime(RFC1123_DATE_FORMAT) + resp = self.client.get('/files/%s' % file_id, + headers={'if_modified_since': updated}) + self.assertEqual(200, resp.status_code) diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 00000000..2a1acdfc --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,3 @@ +from settings import * + +from eve.tests.test_settings import MONGO_DBNAME