Added unittests for caching of file links.

Also converted test_auth.py to use the new AbstractPillarTest class.
This class ensures that we test against the testing database, which
is dropped at every setUp()/tearDown().
This commit is contained in:
Sybren A. Stüvel 2016-03-25 15:57:17 +01:00
parent cb4b0f1e4d
commit adb4f5b39e
5 changed files with 440 additions and 54 deletions

View File

@ -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")

View File

@ -1,23 +1,7 @@
import unittest
import os
import base64 import base64
import httpretty import httpretty
import json
BLENDER_ID_ENDPOINT = 'http://127.0.0.1:8001' # nonexistant server, no trailing slash! from common_test_stuff import AbstractPillarTest, TEST_EMAIL_USER, TEST_EMAIL_ADDRESS
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
def make_header(username, password=''): def make_header(username, password=''):
@ -26,19 +10,11 @@ def make_header(username, password=''):
return 'basic ' + base64.b64encode('%s:%s' % (username, password)) return 'basic ' + base64.b64encode('%s:%s' % (username, password))
class AuthenticationTests(unittest.TestCase): class AuthenticationTests(AbstractPillarTest):
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()
def test_make_unique_username(self): 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. # This user shouldn't exist yet.
self.assertEqual(TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS)) 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') auth.create_new_user(TEST_EMAIL_ADDRESS, TEST_EMAIL_USER, 'test1234')
self.assertEqual('%s1' % TEST_EMAIL_USER, auth.make_unique_username(TEST_EMAIL_ADDRESS)) 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 @httpretty.activate
def test_validate_token__not_logged_in(self): 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()) self.assertFalse(auth.validate_token())
@httpretty.activate @httpretty.activate
def test_validate_token__unknown_token(self): def test_validate_token__unknown_token(self):
"""Test validating of invalid token, unknown both to us and Blender ID.""" """Test validating of invalid token, unknown both to us and Blender ID."""
self.blenderid_validate_unhappy() from application.utils import authentication as auth
with app.test_request_context(headers={'Authorization': make_header('unknowntoken')}):
self.htp_blenderid_validate_unhappy()
with self.app.test_request_context(headers={'Authorization': make_header('unknowntoken')}):
self.assertFalse(auth.validate_token()) self.assertFalse(auth.validate_token())
@httpretty.activate @httpretty.activate
def test_validate_token__unknown_but_valid_token(self): def test_validate_token__unknown_but_valid_token(self):
"""Test validating of valid token, unknown to us but known to Blender ID.""" """Test validating of valid token, unknown to us but known to Blender ID."""
self.blenderid_validate_happy() from application.utils import authentication as auth
with app.test_request_context(headers={'Authorization': make_header('knowntoken')}):
self.htp_blenderid_validate_happy()
with self.app.test_request_context(headers={'Authorization': make_header('knowntoken')}):
self.assertTrue(auth.validate_token()) self.assertTrue(auth.validate_token())

282
tests/test_data.py Normal file
View File

@ -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')}

View File

@ -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)

3
tests/test_settings.py Normal file
View File

@ -0,0 +1,3 @@
from settings import *
from eve.tests.test_settings import MONGO_DBNAME