pillar/tests/test_api/test_bcloud_home_project.py
Sybren A. Stüvel f8ff30fb4d (un)delete on project also (un)delete file documents.
Note that undeleting files cannot be done via Eve, as it doesn't support
PATCHing collections. Instead, direct MongoDB modification is used to set
_deleted=False and provide new _etag and _updated values.
2018-01-31 10:08:17 +01:00

586 lines
26 KiB
Python

# -*- encoding: utf-8 -*-
"""Unit tests for the Blender Cloud home project module."""
import json
import logging
import responses
from bson import ObjectId
from flask import url_for
from pillar.tests import AbstractPillarTest, TEST_EMAIL_ADDRESS
from werkzeug import exceptions as wz_exceptions
log = logging.getLogger(__name__)
class AbstractHomeProjectTest(AbstractPillarTest):
def setUp(self, **kwargs):
AbstractPillarTest.setUp(self, **kwargs)
self.create_standard_groups()
def _create_user_with_token(self, roles, token, user_id='cafef00df00df00df00df00d'):
"""Creates a user directly in MongoDB, so that it doesn't trigger any Eve hooks.
Adds the 'homeproject' role too, which we need to get past the AB-testing.
"""
user_id = self.create_user(roles=roles.union({'homeproject'}), user_id=user_id)
self.create_valid_auth_token(user_id, token)
return user_id
class HomeProjectTest(AbstractHomeProjectTest):
def test_create_home_project(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
user_id = self._create_user_with_token(roles={'subscriber'}, token='token')
# Test home project creation
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
validate_token()
proj = home_project.create_home_project(user_id, write_access=True)
self.assertEqual('home', proj['category'])
self.assertEqual({'group', 'asset', 'comment'},
set(nt['name'] for nt in proj['node_types']))
endpoint = url_for('blender_cloud.home_project.home_project')
db_proj = self.app.data.driver.db['projects'].find_one(proj['_id'])
# Test availability at end-point
resp = self.client.get(endpoint)
# While we're still AB-testing, unauthenticated users should get a 404.
# When that's over, it should result in a 403.
self.assertEqual(403, resp.status_code)
resp = self.client.get(endpoint, headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code)
json_proj = json.loads(resp.data)
self.assertEqual(ObjectId(json_proj['_id']), proj['_id'])
self.assertEqual(json_proj['_etag'], db_proj['_etag'])
@responses.activate
def test_autocreate_home_project_with_subscriber_role(self):
# Implicitly create user by token validation.
self.mock_blenderid_validate_happy()
resp = self.client.get('/api/users/me', headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp)
# Grant subscriber and homeproject roles, and fetch the home project.
self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant')
resp = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code)
json_proj = json.loads(resp.data)
self.assertEqual('home', json_proj['category'])
self.assertEqual('home', json_proj['url'])
# Check that a Blender Sync node was created automatically.
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
nodes_coll = self.app.data.driver.db['nodes']
node = nodes_coll.find_one({
'project': ObjectId(json_proj['_id']),
'node_type': 'group',
'name': 'Blender Sync',
})
self.assertIsNotNone(node)
@responses.activate
def test_autocreate_home_project_with_demo_role(self):
# Implicitly create user by token validation.
self.mock_blenderid_validate_happy()
resp = self.client.get('/api/users/me', headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp)
# Grant demo and homeproject role, which should allow creation of the home project.
self.badger(TEST_EMAIL_ADDRESS, {'demo', 'homeproject'}, 'grant')
resp = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code)
json_proj = json.loads(resp.data)
self.assertEqual('home', json_proj['category'])
self.assertEqual('home', json_proj['url'])
# Check that a Blender Sync node was created automatically.
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
nodes_coll = self.app.data.driver.db['nodes']
node = nodes_coll.find_one({
'project': ObjectId(json_proj['_id']),
'node_type': 'group',
'name': 'Blender Sync',
})
self.assertIsNotNone(node)
@responses.activate
def test_autocreate_home_project_with_succubus_role(self):
from pillar.api.utils import dumps
# Implicitly create user by token validation.
self.mock_blenderid_validate_happy()
resp = self.client.get('/api/users/me', headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp.data)
user_id = ObjectId(json.loads(resp.data)['_id'])
# Grant succubus role, which should allow creation of a read-only home project.
self.badger(TEST_EMAIL_ADDRESS, {'succubus', 'homeproject'}, 'grant')
resp = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code)
json_proj = json.loads(resp.data)
self.assertEqual('home', json_proj['category'])
self.assertEqual('home', json_proj['url'])
# Check that the admin group of the project only has GET permissions.
self.assertEqual({'GET'}, set(json_proj['permissions']['groups'][0]['methods']))
project_id = ObjectId(json_proj['_id'])
admin_group_id = json_proj['permissions']['groups'][0]['group']
# Check that a Blender Sync node was created automatically.
expected_node_permissions = {'users': [],
'groups': [
{'group': ObjectId(admin_group_id),
'methods': ['GET', 'PUT', 'POST', 'DELETE']}, ],
'world': []}
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
nodes_coll = self.app.data.driver.db['nodes']
node = nodes_coll.find_one({
'project': project_id,
'node_type': 'group',
'name': 'Blender Sync',
})
self.assertIsNotNone(node)
# Check that the node itself has write permissions for the admin group.
node_perms = node['permissions']
self.assertEqual(node_perms, expected_node_permissions)
sync_node_id = node['_id']
# Check that we can create a group node inside the sync node.
sub_sync_node = {'project': project_id,
'node_type': 'group',
'parent': sync_node_id,
'name': '2.77',
'user': user_id,
'description': 'Sync folder for Blender 2.77',
'properties': {'status': 'published'},
}
resp = self.client.post('/api/nodes', data=dumps(sub_sync_node),
headers={'Authorization': self.make_header('token'),
'Content-Type': 'application/json'}
)
self.assertEqual(201, resp.status_code, resp.data)
sub_node_info = json.loads(resp.data)
# Check the explicit node-level permissions are copied.
# These aren't returned by the POST to Eve, so we have to check them in the DB manually.
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
nodes_coll = self.app.data.driver.db['nodes']
sub_node = nodes_coll.find_one(ObjectId(sub_node_info['_id']))
node_perms = sub_node['permissions']
self.assertEqual(node_perms, expected_node_permissions)
def test_has_home_project(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
user_id = self._create_user_with_token(roles={'subscriber'}, token='token')
# Test home project creation
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
validate_token()
self.assertFalse(home_project.has_home_project(user_id))
proj = home_project.create_home_project(user_id, write_access=True)
self.assertTrue(home_project.has_home_project(user_id))
# Delete the project.
resp = self.client.delete('/api/projects/%s' % proj['_id'],
headers={'Authorization': self.make_header('token'),
'If-Match': proj['_etag']})
self.assertEqual(204, resp.status_code, resp.data)
self.assertFalse(home_project.has_home_project(user_id))
@responses.activate
def test_home_project_projections(self):
"""Getting the home project should support projections."""
# Implicitly create user by token validation.
self.mock_blenderid_validate_happy()
resp = self.client.get('/api/users/me', headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp)
# Grant subscriber role, and fetch the home project.
self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant')
resp = self.client.get('/api/bcloud/home-project',
query_string={'projection': json.dumps(
{'permissions': 1,
'category': 1,
'user': 1})},
headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp.data)
json_proj = json.loads(resp.data)
self.assertNotIn('name', json_proj)
self.assertNotIn('node_types', json_proj)
self.assertEqual('home', json_proj['category'])
@responses.activate
def test_home_project_url(self):
"""The home project should have 'home' as URL."""
# Implicitly create user by token validation.
self.mock_blenderid_validate_happy()
resp = self.client.get('/api/users/me', headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp)
# Grant subscriber role, and fetch the home project.
self.badger(TEST_EMAIL_ADDRESS, {'subscriber', 'homeproject'}, 'grant')
resp = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token')})
self.assertEqual(200, resp.status_code, resp.data)
json_proj = json.loads(resp.data)
self.assertEqual('home', json_proj['url'])
@responses.activate
def test_multiple_users_with_home_project(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
uid1 = self._create_user_with_token(roles={'subscriber'}, token='token1', user_id=24 * 'a')
uid2 = self._create_user_with_token(roles={'subscriber'}, token='token2', user_id=24 * 'b')
# Create home projects
with self.app.test_request_context(headers={'Authorization': self.make_header('token1')}):
validate_token()
proj1 = home_project.create_home_project(uid1, write_access=True)
db_proj1 = self.app.data.driver.db['projects'].find_one(proj1['_id'])
with self.app.test_request_context(headers={'Authorization': self.make_header('token2')}):
validate_token()
proj2 = home_project.create_home_project(uid2, write_access=True)
db_proj2 = self.app.data.driver.db['projects'].find_one(proj2['_id'])
# Test availability at end-point
resp1 = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token1')})
resp2 = self.client.get('/api/bcloud/home-project',
headers={'Authorization': self.make_header('token2')})
self.assertEqual(200, resp1.status_code)
self.assertEqual(200, resp2.status_code)
json_proj1 = json.loads(resp1.data)
json_proj2 = json.loads(resp2.data)
self.assertEqual(ObjectId(json_proj1['_id']), proj1['_id'])
self.assertEqual(ObjectId(json_proj2['_id']), proj2['_id'])
self.assertEqual(json_proj1['_etag'], db_proj1['_etag'])
self.assertEqual(json_proj2['_etag'], db_proj2['_etag'])
self.assertNotEqual(db_proj1['_etag'], db_proj2['_etag'])
self.assertNotEqual(db_proj1['_id'], db_proj2['_id'])
def test_delete_restore(self):
"""Deleting and then recreating a home project should restore the deleted project."""
self._create_user_with_token(roles={'subscriber'}, token='token')
# Create home project by getting it.
resp = self.get('/api/bcloud/home-project', auth_token='token')
before_delete_json_proj = resp.json()
# Delete the project.
self.delete(f'/api/projects/{before_delete_json_proj["_id"]}',
auth_token='token', etag=before_delete_json_proj['_etag'],
expected_status=204)
# Recreate home project by getting it.
resp = self.get('/api/bcloud/home-project', auth_token='token')
after_delete_json_proj = resp.json()
self.assertEqual(before_delete_json_proj['_id'],
after_delete_json_proj['_id'])
class HomeProjectUserChangedRoleTest(AbstractPillarTest):
def setUp(self, **kwargs):
AbstractPillarTest.setUp(self, **kwargs)
self.create_standard_groups()
def test_without_home_project(self):
from pillar.api.blender_cloud import home_project
self.user_id = self.create_user()
with self.app.test_request_context():
changed = home_project.user_changed_role(None, {'_id': self.user_id})
self.assertFalse(changed)
# Shouldn't do anything, shouldn't crash either.
def test_already_subscriber_role(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
self.user_id = self.create_user(roles=set('subscriber'))
self.create_valid_auth_token(self.user_id, 'token')
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
validate_token()
home_proj = home_project.create_home_project(self.user_id, write_access=True)
changed = home_project.user_changed_role(None, {'_id': self.user_id,
'roles': ['subscriber']})
self.assertFalse(changed)
# The home project should still be writable, so we should be able to create a node.
self.create_test_node(home_proj['_id'])
def test_granting_subscriber_role(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
self.user_id = self.create_user(roles=set())
self.create_valid_auth_token(self.user_id, 'token')
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
validate_token()
home_proj = home_project.create_home_project(self.user_id, write_access=False)
changed = home_project.user_changed_role(None, {'_id': self.user_id,
'roles': ['subscriber']})
self.assertTrue(changed)
# The home project should be writable, so we should be able to create a node.
self.create_test_node(home_proj['_id'])
def test_revoking_subscriber_role(self):
from pillar.api.blender_cloud import home_project
from pillar.api.utils.authentication import validate_token
self.user_id = self.create_user(roles=set('subscriber'))
self.create_valid_auth_token(self.user_id, 'token')
with self.app.test_request_context(headers={'Authorization': self.make_header('token')}):
validate_token()
home_proj = home_project.create_home_project(self.user_id, write_access=True)
changed = home_project.user_changed_role(None, {'_id': self.user_id,
'roles': []})
self.assertTrue(changed)
# The home project should NOT be writable, so we should NOT be able to create a node.
self.create_test_node(home_proj['_id'], 403)
def create_test_node(self, project_id, status_code=201):
from pillar.api.utils import dumps
node = {
'project': project_id,
'node_type': 'group',
'name': 'test group node',
'user': self.user_id,
'properties': {},
}
resp = self.client.post('/api/nodes', data=dumps(node),
headers={'Authorization': self.make_header('token'),
'Content-Type': 'application/json'})
self.assertEqual(status_code, resp.status_code, resp.data)
class TextureLibraryTest(AbstractHomeProjectTest):
def setUp(self, **kwargs):
AbstractHomeProjectTest.setUp(self, **kwargs)
user_id = self._create_user_with_token(set(), 'token')
self.hdri_proj_id, proj = self.ensure_project_exists(project_overrides={'_id': 24 * 'a'})
self.tex_proj_id, proj2 = self.ensure_project_exists(project_overrides={'_id': 24 * 'b'})
self.create_node({'description': '',
'project': self.hdri_proj_id,
'node_type': 'group_hdri',
'user': user_id,
'properties': {'status': 'published',
'tags': [],
'order': 0,
'categories': '',
'files': ''},
'name': 'HDRi test node'}
)
self.create_node({'description': '',
'project': self.tex_proj_id,
'node_type': 'group_texture',
'user': user_id,
'properties': {'status': 'published',
'tags': [],
'order': 0,
'categories': '',
'files': ''},
'name': 'Group texture test node'}
)
def test_blender_cloud_addon_version(self):
from pillar.api.blender_cloud import blender_cloud_addon_version
# Three-digit version
with self.app.test_request_context(headers={'Blender-Cloud-Addon': '1.3.3'}):
self.assertEqual((1, 3, 3), blender_cloud_addon_version())
# Two-digit version
with self.app.test_request_context(headers={'Blender-Cloud-Addon': '1.5'}):
self.assertEqual((1, 5), blender_cloud_addon_version())
# No version
with self.app.test_request_context():
self.assertEqual(None, blender_cloud_addon_version())
# Malformed version
with self.app.test_request_context(headers={'Blender-Cloud-Addon': 'je moeder'}):
self.assertRaises(wz_exceptions.BadRequest, blender_cloud_addon_version)
def test_hdri_library__no_bcloud_version(self):
resp = self.get('/api/bcloud/texture-libraries', auth_token='token')
libs = resp.json()['_items']
library_project_ids = {proj['_id'] for proj in libs}
self.assertNotIn(str(self.hdri_proj_id), library_project_ids)
self.assertIn(str(self.tex_proj_id), library_project_ids)
def test_hdri_library__old_bcloud_addon(self):
resp = self.get('/api/bcloud/texture-libraries',
auth_token='token',
headers={'Blender-Cloud-Addon': '1.3.3'})
libs = resp.json()['_items']
library_project_ids = {proj['_id'] for proj in libs}
self.assertNotIn(str(self.hdri_proj_id), library_project_ids)
self.assertIn(str(self.tex_proj_id), library_project_ids)
def test_hdri_library__new_bcloud_addon(self):
resp = self.get('/api/bcloud/texture-libraries',
auth_token='token',
headers={'Blender-Cloud-Addon': '1.4.0'})
libs = resp.json()['_items']
library_project_ids = {proj['_id'] for proj in libs}
self.assertIn(str(self.hdri_proj_id), library_project_ids)
self.assertIn(str(self.tex_proj_id), library_project_ids)
class HdriSortingTest(AbstractHomeProjectTest):
def setUp(self, **kwargs):
from pillar.api.node_types.hdri import node_type_hdri
super(HdriSortingTest, self).setUp(**kwargs)
self.user_id = self._create_user_with_token({'subscriber'}, 'token')
self.hdri_proj_id, proj = self.ensure_project_exists(project_overrides={
'user': self.user_id,
'permissions': {'world': ['DELETE', 'GET', 'POST', 'PUT']},
'node_types': [node_type_hdri],
})
self.hdri_proj_id = ObjectId(self.hdri_proj_id)
self.assertIsNotNone(self.hdri_proj_id)
# Add some test files.
with self.app.test_request_context():
files_coll = self.app.data.driver.db['files']
self.file_256p = files_coll.insert_one({
'name': '96f1adf5330a4cadbf73c13d718ac163.hdr',
'format': 'radiance-hdr',
'filename': 'symmetrical_garden_256p.hdr',
'project': self.hdri_proj_id,
'length': 106435,
'user': self.user_id,
'content_type': 'application/octet-stream',
'file_path': '96f1adf5330a4cadbf73c13d718ac163.hdr'}).inserted_id
self.file_1k = files_coll.insert_one({
'name': '96f1adf5330a4cadbf73qweqwecqwev142v.hdr',
'format': 'radiance-hdr',
'filename': 'symmetrical_garden_1k.hdr',
'project': self.hdri_proj_id,
'length': 1431435,
'user': self.user_id,
'content_type': 'application/octet-stream',
'file_path': 'd13rfc13r1evadbf73c13d718ac163.hdr'}).inserted_id
self.file_2k = files_coll.insert_one({
'name': '34134df5330a4cadbf73qweqwecqwev142v.hdr',
'format': 'radiance-hdr',
'filename': 'symmetrical_garden_2k.hdr',
'project': self.hdri_proj_id,
'length': 2431435,
'user': self.user_id,
'content_type': 'application/octet-stream',
'file_path': 'd13rfc13r1evadbf73c13d718ac163.hdr'}).inserted_id
def test_hdri_sorting_on_create(self):
# Create node with files in wrong order
node = {'project': self.hdri_proj_id,
'node_type': 'hdri',
'user': self.user_id,
'properties': {
'files': [
{'resolution': '2k', 'file': self.file_2k},
{'resolution': '256p', 'file': self.file_256p},
{'resolution': '1k', 'file': self.file_1k},
],
'status': 'published',
},
'name': 'Symmetrical Garden'
}
resp = self.post('/api/nodes', json=node, expected_status=201, auth_token='token')
node_info = resp.json()
# Check that the node's files are in the right order
resp = self.get('/api/nodes/%s' % node_info['_id'], auth_token='token')
get_node = resp.json()
self.assertEqual(['256p', '1k', '2k'],
[file_info['resolution']
for file_info in get_node['properties']['files']])
def test_hdri_sorting_on_update(self):
# Create node with files in correct order
node = {'project': self.hdri_proj_id,
'node_type': 'hdri',
'user': self.user_id,
'properties': {
'files': [
{'resolution': '256p', 'file': self.file_256p},
{'resolution': '1k', 'file': self.file_1k},
{'resolution': '2k', 'file': self.file_2k},
],
'status': 'published',
},
'name': 'Symmetrical Garden'
}
resp = self.post('/api/nodes', json=node, expected_status=201, auth_token='token')
node_info = resp.json()
# Mess up the node's order
node['properties']['files'] = [
{'resolution': '2k', 'file': self.file_2k},
{'resolution': '1k', 'file': self.file_1k},
{'resolution': '256p', 'file': self.file_256p},
]
self.put('/api/nodes/%s' % node_info['_id'], json=node, auth_token='token',
headers={'If-Match': node_info['_etag']})
# Check that the node's files are in the right order
resp = self.get('/api/nodes/%s' % node_info['_id'], auth_token='token')
get_node = resp.json()
self.assertEqual(['256p', '1k', '2k'],
[file_info['resolution']
for file_info in get_node['properties']['files']])