From 15e745717cd10155ce86c426964f396cbc61153a Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 26 Jun 2017 15:43:59 -0400 Subject: [PATCH] fill out tests for v1 server API * Count the number of times a mock corountine has been awaited. * Expand handler tests, port them to pytest and reorganize fixtures --- tests/core/test_metadata.py | 8 +- tests/server/api/v1/fixtures.py | 273 ++++++++++++++++ tests/server/api/v1/fixtures/fixtures.json | 89 ++++++ tests/server/api/v1/test_core.py | 42 +++ tests/server/api/v1/test_create_mixin.py | 295 +++++++++++------- tests/server/api/v1/test_metadata_mixin.py | 212 ++++++++----- tests/server/api/v1/test_movecopy.py | 133 ++++++++ tests/server/api/v1/test_provider.py | 225 +++++++++++++ tests/utils.py | 36 ++- .../server/api/v1/provider/__init__.py | 1 - 10 files changed, 1112 insertions(+), 202 deletions(-) create mode 100644 tests/server/api/v1/fixtures.py create mode 100644 tests/server/api/v1/fixtures/fixtures.json create mode 100644 tests/server/api/v1/test_core.py create mode 100644 tests/server/api/v1/test_movecopy.py create mode 100644 tests/server/api/v1/test_provider.py diff --git a/tests/core/test_metadata.py b/tests/core/test_metadata.py index d072e2cbd..823183631 100644 --- a/tests/core/test_metadata.py +++ b/tests/core/test_metadata.py @@ -34,8 +34,8 @@ def test_file_json_api_serialize(self): 'materialized': '/Foo.name', 'etag': etag, 'contentType': 'application/octet-stream', - 'modified': 'never', - 'modified_utc': 'never', + 'modified': '9/25/2017', + 'modified_utc': '1991-09-25T19:20:30.45+01:00', 'created_utc': 'always', 'size': 1337, 'sizeInt': 1337, @@ -90,8 +90,8 @@ def test_folder_json_api_size_serialize(self): 'materialized': '/Foo.name', 'etag': etag, 'contentType': 'application/octet-stream', - 'modified': 'never', - 'modified_utc': 'never', + 'modified': '9/25/2017', + 'modified_utc': '1991-09-25T19:20:30.45+01:00', 'created_utc': 'always', 'size': 1337, 'sizeInt': 1337, diff --git a/tests/server/api/v1/fixtures.py b/tests/server/api/v1/fixtures.py new file mode 100644 index 000000000..3e7e3c28e --- /dev/null +++ b/tests/server/api/v1/fixtures.py @@ -0,0 +1,273 @@ +import os +import sys +import json +import asyncio +from unittest import mock + +import pytest +from tornado.web import HTTPError +from tornado.httputil import HTTPServerRequest +from tornado.http1connection import HTTP1ConnectionParameters + +import waterbutler +from waterbutler.server.app import make_app +from waterbutler.core.path import WaterButlerPath +from waterbutler.tasks.exceptions import WaitTimeOutError +from waterbutler.server.api.v1.provider import ProviderHandler +from tests.utils import (MockProvider, MockFileMetadata, MockFolderMetadata, + MockFileRevisionMetadata, MockCoroutine, MockRequestBody, MockStream) + + +@pytest.fixture +def http_request(): + mocked_http_request = HTTPServerRequest( + uri='/v1/resources/test/providers/test/path/mock', + method='GET' + ) + mocked_http_request.headers['User-Agent'] = 'test' + mocked_http_request.connection = HTTP1ConnectionParameters() + mocked_http_request.connection.set_close_callback = mock.Mock() + mocked_http_request.request_time = mock.Mock(return_value=10) + mocked_http_request.body = MockRequestBody() + return mocked_http_request + + +@pytest.fixture +def handler(http_request): + mocked_handler = ProviderHandler(make_app(True), http_request) + + mocked_handler.path_kwargs = { + 'provider': 'test', + 'path': '/file', + 'resource': 'guid1' + } + mocked_handler.path = '/test_path' + mocked_handler.provider = MockProvider() + + mocked_handler.resource = 'test_source_resource' + mocked_handler.metadata = MockFileMetadata() + + mocked_handler.dest_path = '/test_dest_path' + mocked_handler.dest_provider = MockProvider() + mocked_handler.dest_resource = 'test_dest_resource' + mocked_handler.dest_meta = MockFileMetadata() + mocked_handler.arguments = {} + mocked_handler.write = mock.Mock() + mocked_handler.write_stream = MockCoroutine() + mocked_handler.redirect = mock.Mock() + mocked_handler.uploader = asyncio.Future() + mocked_handler.wsock = mock.Mock() + mocked_handler.writer = mock.Mock() + + return mocked_handler + + +@pytest.fixture +def mock_stream(): + return MockStream() + + +@pytest.fixture +def mock_partial_stream(): + stream = MockStream() + stream.partial = True + stream.content_range = 'bytes=10-100' + return stream + + +@pytest.fixture() +def mock_file_metadata(): + return MockFileMetadata() + + +@pytest.fixture() +def mock_folder_metadata(): + return MockFolderMetadata() + + +@pytest.fixture() +def mock_revision_metadata(): + return [MockFileRevisionMetadata()] + + +@pytest.fixture() +def mock_folder_children(): + return [MockFolderMetadata(), MockFileMetadata(), MockFileMetadata()] + + +@pytest.fixture +def patch_auth_handler(monkeypatch, handler_auth): + mock_auth_handler = MockCoroutine(return_value=handler_auth) + monkeypatch.setattr(waterbutler.server.auth.AuthHandler, 'get', mock_auth_handler) + return mock_auth_handler + + +@pytest.fixture +def patch_make_provider_move_copy(monkeypatch): + make_provider = mock.Mock(return_value=MockProvider()) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy, 'make_provider', make_provider) + return make_provider + + +@pytest.fixture +def patch_make_provider_core(monkeypatch): + make_provider = mock.Mock(return_value=MockProvider()) + monkeypatch.setattr(waterbutler.server.api.v1.provider.utils, 'make_provider', make_provider) + return make_provider + + +@pytest.fixture(params=[True, False]) +def mock_intra(monkeypatch, request): + src_provider = MockProvider() + dest_provider = MockProvider() + mock_make_provider = mock.Mock(side_effect=[src_provider, dest_provider]) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy, + 'make_provider', + mock_make_provider) + + src_provider.can_intra_copy = mock.Mock(return_value=True) + src_provider.can_intra_move = mock.Mock(return_value=True) + + mock_backgrounded = MockCoroutine(return_value=(MockFileMetadata(), request.param)) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy.tasks, + 'backgrounded', + mock_backgrounded) + + return mock_make_provider, mock_backgrounded + + +@pytest.fixture(params=[True, False]) +def mock_inter(monkeypatch, request): + src_provider = MockProvider() + dest_provider = MockProvider() + mock_make_provider = mock.Mock(side_effect=[src_provider, dest_provider]) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy, + 'make_provider', + mock_make_provider) + + mock_celery = MockCoroutine(return_value=(MockFileMetadata(), request.param)) + mock_adelay = MockCoroutine(return_value='4ef2d1dd-c5da-41a7-ae4a-9d0ba7a68927') + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy.tasks.copy, + 'adelay', + mock_adelay) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy.tasks.move, + 'adelay', + mock_adelay) + monkeypatch.setattr(waterbutler.server.api.v1.provider.movecopy.tasks, + 'wait_on_celery', + mock_celery) + + return mock_make_provider, mock_adelay + + +@pytest.fixture +def mock_exc_info(): + try: + raise Exception('test exception') + except: + return sys.exc_info() + + +@pytest.fixture +def mock_exc_info_http(): + try: + raise HTTPError(status_code=500, log_message='test http exception') + except HTTPError: + return sys.exc_info() + + +@pytest.fixture +def mock_exc_info_202(): + try: + raise WaitTimeOutError('test exception') + except WaitTimeOutError: + return sys.exc_info() + + +@pytest.fixture +def move_copy_args(): + return ( + { + 'nid': 'test_source_resource', + 'provider': { + 'credentials': {}, + 'name': 'MockProvider', + 'auth': {}, + 'settings': {} + }, + 'path': '/test_path' + }, + { + 'nid': 'test_dest_resource', + 'provider': { + 'credentials': {}, + 'name': 'MockProvider', + 'auth': {}, + 'settings': {} + }, 'path': '/test_dest_path' + } + ) + + +@pytest.fixture +def celery_src_copy_params(): + return { + 'nid': 'test_source_resource', + 'path': WaterButlerPath('/test_path', prepend=None), + 'provider': { + 'credentials': {}, + 'name': 'MockProvider', + 'settings': {}, + 'auth': {} + } + } + + +@pytest.fixture +def celery_dest_copy_params(): + return { + 'nid': 'test_source_resource', + 'path': WaterButlerPath('/test_path/', prepend=None), + 'provider': { + 'credentials': {}, + 'name': 'MockProvider', + 'settings': {}, + 'auth': {} + } + } + + +@pytest.fixture +def celery_dest_copy_params_root(): + return { + 'nid': 'test_source_resource', + 'path': WaterButlerPath('/', prepend=None), + 'provider': { + 'credentials': {}, + 'name': 'MockProvider', + 'settings': {}, + 'auth': {} + } + } + + +@pytest.fixture +def handler_auth(): + with open(os.path.join(os.path.dirname(__file__), 'fixtures/fixtures.json'), 'r') as fp: + return json.load(fp)['hander_auth'] + + +@pytest.fixture +def serialized_metadata(): + with open(os.path.join(os.path.dirname(__file__), 'fixtures/fixtures.json'), 'r') as fp: + return json.load(fp)['serialized_metadata'] + + +@pytest.fixture +def serialized_request(): + with open(os.path.join(os.path.dirname(__file__), 'fixtures/fixtures.json'), 'r') as fp: + return json.load(fp)['serialized_request'] + + + + diff --git a/tests/server/api/v1/fixtures/fixtures.json b/tests/server/api/v1/fixtures/fixtures.json new file mode 100644 index 000000000..0d8f0ab89 --- /dev/null +++ b/tests/server/api/v1/fixtures/fixtures.json @@ -0,0 +1,89 @@ +{ + "hander_auth": { + "callback_url": "http://192.168.168.167:5000/api/v1/project/fmsaw/waterbutler/logs/", + "credentials": { + "storage": {} + }, + "settings": { + "rootId": "5a4be6acfc93fd00108dab0c", + "nid": "fmsaw", + "storage": { + "folder": "/code/website/osfstoragecache", + "provider": "test" + }, + "folder": "/folder/", + "baseUrl": "http://192.168.168.167:5000/api/v1/project/fmsaw/osfstorage/" + }, + "auth": { + "callback_url": "http://192.168.168.167:5000/api/v1/project/fmsaw/waterbutler/logs/", + "name": "fake", + "id": "3jc7m", + "email": "3jc7m@osf.io" + } + }, + "serialized_metadata": { + "data": { + "attributes": { + "contentType": "application/octet-stream", + "materialized": "/Foo.name", + "created_utc": "always", + "etag": "4ac022b0d4aa61eca8f5e114013a740f784b7d72f237fce49b305d5c95278f64", + "modified": "9/25/2017", + "extra": {}, + "modified_utc": "1991-09-25T19:20:30.45+01:00", + "kind": "file", + "resource": "test_source_resource", + "size": 1337, + "sizeInt": 1337, + "name": "Foo.name", + "path": "/Foo.name", + "provider": "MockProvider" + }, + "id": "MockProvider/Foo.name", + "links": { + "download": "http://localhost:7777/v1/resources/test_source_resource/providers/MockProvider/Foo.name", + "delete": "http://localhost:7777/v1/resources/test_source_resource/providers/MockProvider/Foo.name", + "upload": "http://localhost:7777/v1/resources/test_source_resource/providers/MockProvider/Foo.name?kind=file", + "move": "http://localhost:7777/v1/resources/test_source_resource/providers/MockProvider/Foo.name" + }, + "type": "files" + } + }, + "serialized_request": { + "referrer": { + "url": null + }, + "tech": { + "ip": null, + "ua": "test" + }, + "request": { + "headers": {}, + "method": "GET", + "url": "http://127.0.0.1/v1/resources/test/providers/test/path/mock", + "time": 10 + } + }, + "move_copy_args": [ + { + "nid": "test_source_resource", + "provider": { + "credentials": {}, + "name": "MockProvider", + "auth": {}, + "settings": {} + }, + "path": "/test_path" + }, + { + "nid": "test_dest_resource", + "provider": { + "credentials": {}, + "name": "MockProvider", + "auth": {}, + "settings": {} + }, + "path": "/test_dest_path" + } + ] +} diff --git a/tests/server/api/v1/test_core.py b/tests/server/api/v1/test_core.py new file mode 100644 index 000000000..441bf6289 --- /dev/null +++ b/tests/server/api/v1/test_core.py @@ -0,0 +1,42 @@ +from unittest import mock + +from tests.server.api.v1.fixtures import (http_request, handler, mock_exc_info, mock_exc_info_202, + mock_exc_info_http) + + +class TestBaseHandler: + + def test_write_error(self, handler, mock_exc_info): + handler.finish = mock.Mock() + handler.captureException = mock.Mock() + + handler.write_error(500, mock_exc_info) + + handler.finish.assert_called_with({'message': 'OK', 'code': 500}) + handler.captureException.assert_called_with(mock_exc_info) + + def test_write_error_202(self, handler, mock_exc_info_202): + handler.finish = mock.Mock() + handler.captureException = mock.Mock() + + handler.write_error(500, mock_exc_info_202) + + handler.finish.assert_called_with() + handler.captureException.assert_called_with(mock_exc_info_202, data={'level': 'info'}) + + @mock.patch('tornado.web.app_log.error') + def test_log_exception_uncaught(self, mocked_error, handler, mock_exc_info): + + handler.log_exception(*mock_exc_info) + + mocked_error.assert_called_with('Uncaught exception %s\n', + 'GET /v1/resources/test/providers/test/path/mock (None)', + exc_info=mock_exc_info) + + @mock.patch('tornado.web.gen_log.warning') + def test_log_exception_http_error(self, mocked_warning, handler, mock_exc_info_http): + handler.log_exception(*mock_exc_info_http) + + mocked_warning.assert_called_with('%d %s: test http exception', + 500, + 'GET /v1/resources/test/providers/test/path/mock (None)') diff --git a/tests/server/api/v1/test_create_mixin.py b/tests/server/api/v1/test_create_mixin.py index 0a29aff3d..6ef9e0488 100644 --- a/tests/server/api/v1/test_create_mixin.py +++ b/tests/server/api/v1/test_create_mixin.py @@ -1,165 +1,234 @@ -import pytest -import asyncio from http import client from unittest import mock +import pytest + +from tests.utils import MockCoroutine from waterbutler.core import exceptions from waterbutler.core.path import WaterButlerPath -from waterbutler.server.api.v1.provider.create import CreateMixin +from tests.server.api.v1.fixtures import (http_request, handler, handler_auth, mock_folder_metadata, + mock_file_metadata) -from tests.utils import MockCoroutine +class TestValidatePut: + + @pytest.mark.asyncio + async def test_postvalidate_put_file(self, handler): + handler.path = WaterButlerPath('/file') + handler.kind = 'file' + handler.get_query_argument = mock.Mock(return_value=None) + + await handler.postvalidate_put() -class BaseCreateMixinTest: + assert handler.target_path == handler.path + handler.get_query_argument.assert_called_once_with('name', default=None) - def setup_method(self, method): - self.mixin = CreateMixin() - self.mixin.write = mock.Mock() - self.mixin.request = mock.Mock() - self.mixin.set_status = mock.Mock() - self.mixin.get_query_argument = mock.Mock() + @pytest.mark.asyncio + async def test_postvalidate_put_folder(self, handler): + handler.path = WaterButlerPath('/Folder1/') + handler.kind = 'folder' + handler.get_query_argument = mock.Mock(return_value='child!') + handler.provider.exists = MockCoroutine(return_value=False) + handler.provider.can_duplicate_names = MockCoroutine(return_value=False) + await handler.postvalidate_put() -class TestValidatePut(BaseCreateMixinTest): + assert handler.target_path == WaterButlerPath('/Folder1/child!/') + handler.get_query_argument.assert_called_once_with('name', default=None) + handler.provider.exists.assert_called_once_with( + WaterButlerPath('/Folder1/child!', prepend=None)) - def test_invalid_kind(self): - self.mixin.get_query_argument.return_value = 'notaferlder' + @pytest.mark.asyncio + async def test_postvalidate_put_folder_naming_conflict(self, handler): + handler.path = WaterButlerPath('/Folder1/') + handler.kind = 'folder' + handler.get_query_argument = mock.Mock(return_value='child!') + handler.provider.exists = MockCoroutine(return_value=True) - with pytest.raises(exceptions.InvalidParameters) as e: - self.mixin.prevalidate_put() + with pytest.raises(exceptions.NamingConflict) as exc: + await handler.postvalidate_put() - assert e.value.message == 'Kind must be file, folder or unspecified (interpreted as file), not notaferlder' + assert exc.value.message == 'Cannot complete action: file or folder "child!" already ' \ + 'exists in this location' - def test_default_kind(self): - self.mixin.path = '/' - self.mixin.get_query_argument.return_value = 'file' - self.mixin.request.headers.get.side_effect = Exception('Breakout') + assert handler.target_path == WaterButlerPath('/Folder1/child!/') + handler.get_query_argument.assert_called_once_with('name', default=None) + handler.provider.exists.assert_called_once_with( + WaterButlerPath('/Folder1/child!', prepend=None)) - with pytest.raises(Exception) as e: - self.mixin.prevalidate_put() + @pytest.mark.asyncio + async def test_postvalidate_put_cant_duplicate_names(self, handler): + handler.path = WaterButlerPath('/Folder1/') + handler.kind = 'folder' + handler.provider.can_duplicate_names = mock.Mock(return_value=False) + handler.get_query_argument = mock.Mock(return_value='child!') + handler.provider.exists = MockCoroutine(return_value=False) - assert self.mixin.kind == 'file' - assert e.value.args == ('Breakout', ) - assert self.mixin.get_query_argument.has_call(mock.call('kind', default='file')) + await handler.postvalidate_put() + + assert handler.target_path == WaterButlerPath('/Folder1/child!/') + handler.get_query_argument.assert_called_once_with('name', default=None) + handler.provider.exists.assert_called_with(WaterButlerPath('/Folder1/child!', prepend=None)) + handler.provider.can_duplicate_names.assert_called_once_with() + + @pytest.mark.asyncio + async def test_postvalidate_put_cant_duplicate_names_and_naming_conflict(self, handler): + handler.path = WaterButlerPath('/Folder1/') + handler.kind = 'folder' + handler.provider.can_duplicate_names = mock.Mock(return_value=False) + handler.get_query_argument = mock.Mock(return_value='child!') + handler.provider.exists = MockCoroutine(side_effect=[False, True]) - def test_length_required_for_files(self): - self.mixin.path = '/' - self.mixin.request.headers = {} - self.mixin.get_query_argument.return_value = 'file' + with pytest.raises(exceptions.NamingConflict) as exc: + await handler.postvalidate_put() - with pytest.raises(exceptions.InvalidParameters) as e: - self.mixin.prevalidate_put() + assert exc.value.message == 'Cannot complete action: file or folder "child!" already ' \ + 'exists in this location' - assert e.value.code == client.LENGTH_REQUIRED - assert e.value.message == 'Content-Length is required for file uploads' + handler.provider.can_duplicate_names.assert_called_once_with() + handler.get_query_argument.assert_called_once_with('name', default=None) + handler.provider.exists.assert_called_with( + WaterButlerPath('/Folder1/child!', prepend=None)) - def test_payload_with_folder(self): - self.mixin.path = '/' - self.mixin.request.headers = {'Content-Length': 5000} - self.mixin.get_query_argument.return_value = 'folder' + def test_invalid_kind(self, handler): + handler.get_query_argument = mock.Mock(return_value='notafolder') - with pytest.raises(exceptions.InvalidParameters) as e: - self.mixin.prevalidate_put() + with pytest.raises(exceptions.InvalidParameters) as exc: + handler.prevalidate_put() - assert e.value.code == client.REQUEST_ENTITY_TOO_LARGE - assert e.value.message == 'Folder creation requests may not have a body' + handler.get_query_argument.assert_called_once_with('kind', default='file') + assert exc.value.message == 'Kind must be file, folder or unspecified (interpreted as ' \ + 'file), not notafolder' - self.mixin.request.headers = {'Content-Length': 'notanumber'} - self.mixin.get_query_argument.return_value = 'file' + def test_default_kind(self, handler): + handler.get_query_argument = mock.Mock(return_value='file') + handler.request.headers.get = mock.Mock(side_effect=Exception('Breakout')) - with pytest.raises(exceptions.InvalidParameters) as e: - self.mixin.prevalidate_put() + with pytest.raises(Exception) as exc: + handler.prevalidate_put() - assert e.value.code == client.BAD_REQUEST - assert e.value.message == 'Invalid Content-Length' + assert handler.kind == 'file' + assert exc.value.args == ('Breakout', ) + handler.get_query_argument.assert_called_once_with('kind', default='file') + handler.request.headers.get.assert_called_once_with('Content-Length') + + def test_length_required_for_files(self, handler): + handler.request.headers = {} + handler.get_query_argument = mock.Mock(return_value='file') + + with pytest.raises(exceptions.InvalidParameters) as exc: + handler.prevalidate_put() + + assert exc.value.code == client.LENGTH_REQUIRED + assert exc.value.message == 'Content-Length is required for file uploads' + handler.get_query_argument.assert_called_once_with('kind', default='file') + + def test_payload_with_folder(self, handler): + handler.request.headers = {'Content-Length': 5000} + handler.get_query_argument = mock.Mock(return_value='folder') + + with pytest.raises(exceptions.InvalidParameters) as exc: + handler.prevalidate_put() + + assert exc.value.code == client.REQUEST_ENTITY_TOO_LARGE + assert exc.value.message == 'Folder creation requests may not have a body' + handler.get_query_argument.assert_called_once_with('kind', default='file') + + def test_payload_with_invalid_content_length(self, handler): + handler.request.headers = {'Content-Length': 'notanumber'} + handler.get_query_argument = mock.Mock(return_value='file') + + with pytest.raises(exceptions.InvalidParameters) as exc: + handler.prevalidate_put() + + assert exc.value.code == client.BAD_REQUEST + assert exc.value.message == 'Invalid Content-Length' + handler.get_query_argument.assert_called_once_with('kind', default='file') @pytest.mark.asyncio - async def test_name_required_for_dir(self): - self.mixin.path = WaterButlerPath('/', folder=True) - self.mixin.get_query_argument.return_value = None + async def test_name_required_for_dir(self, handler): + handler.path = WaterButlerPath('/', folder=True) + handler.get_query_argument = mock.Mock(return_value=None) - with pytest.raises(exceptions.InvalidParameters) as e: - await self.mixin.postvalidate_put() + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.postvalidate_put() - assert e.value.message == 'Missing required parameter \'name\'' + assert exc.value.message == 'Missing required parameter \'name\'' + handler.get_query_argument.assert_called_once_with('name', default=None) @pytest.mark.asyncio - async def test_name_refused_for_file(self): - self.mixin.path = WaterButlerPath('/foo.txt', folder=False) - self.mixin.get_query_argument.return_value = 'bar.txt' + async def test_name_refused_for_file(self, handler): + handler.path = WaterButlerPath('/foo.txt', folder=False) + handler.get_query_argument = mock.Mock(return_value='bar.txt') - with pytest.raises(exceptions.InvalidParameters) as e: - await self.mixin.postvalidate_put() + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.postvalidate_put() - assert e.value.message == "'name' parameter doesn't apply to actions on files" + assert exc.value.message == "'name' parameter doesn't apply to actions on files" + handler.get_query_argument.assert_called_once_with('name', default=None) @pytest.mark.asyncio - async def test_kind_must_be_folder(self): - self.mixin.path = WaterButlerPath('/adlkjf') - self.mixin.get_query_argument.return_value = None - self.mixin.kind = 'folder' + async def test_kind_must_be_folder(self, handler): + handler.path = WaterButlerPath('/adlkjf') + handler.get_query_argument = mock.Mock(return_value=None) + handler.kind = 'folder' - with pytest.raises(exceptions.InvalidParameters) as e: - await self.mixin.postvalidate_put() + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.postvalidate_put() - assert e.value.message == 'Path must be a folder (and end with a "/") if trying to create a subfolder' - assert e.value.code == client.CONFLICT + assert exc.value.message == 'Path must be a folder (and end with a "/") if trying to ' \ + 'create a subfolder' + assert exc.value.code == client.CONFLICT + handler.get_query_argument.assert_called_once_with('name', default=None) -class TestCreateFolder(BaseCreateMixinTest): +class TestCreateFolder: @pytest.mark.asyncio - async def test_created(self): - metadata = mock.Mock() - self.mixin.path = '/' - self.mixin.resource = '3rqws' - metadata.json_api_serialized.return_value = {'day': 'tum'} - self.mixin.provider = mock.Mock( - create_folder=MockCoroutine(return_value=metadata) - ) - target = WaterButlerPath('/apath/') - self.mixin.target_path = target - - await self.mixin.create_folder() + async def test_create_folder(self, handler, mock_folder_metadata): + handler.resource = '3rqws' + handler.provider.create_folder = MockCoroutine(return_value=mock_folder_metadata) + handler.target_path = WaterButlerPath('/apath/') + handler.set_status = mock.Mock() - assert self.mixin.set_status.assert_called_once_with(201) is None - assert self.mixin.write.assert_called_once_with({'data': {'day': 'tum'}}) is None - assert self.mixin.provider.create_folder.assert_called_once_with(target) is None + await handler.create_folder() + handler.set_status.assert_called_once_with(201) + handler.write.assert_called_once_with({ + 'data': mock_folder_metadata.json_api_serialized('3rqws') + }) + handler.provider.create_folder.assert_called_once_with(WaterButlerPath('/apath/')) -class TestUploadFile(BaseCreateMixinTest): - def setup_method(self, method): - super().setup_method(method) - self.mixin.wsock = mock.Mock() - self.mixin.writer = mock.Mock() +class TestUploadFile: @pytest.mark.asyncio - async def test_created(self): - metadata = mock.Mock() - self.mixin.resource = '3rqws' - self.mixin.uploader = asyncio.Future() - metadata.json_api_serialized.return_value = {'day': 'tum'} - self.mixin.uploader.set_result((metadata, True)) + async def test_created(self, handler, mock_file_metadata): + handler.resource = '3rqws' + handler.uploader.set_result((mock_file_metadata, True)) + handler.set_status = mock.Mock() - await self.mixin.upload_file() + await handler.upload_file() - assert self.mixin.wsock.close.called - assert self.mixin.writer.close.called - assert self.mixin.set_status.assert_called_once_with(201) is None - assert self.mixin.write.assert_called_once_with({'data': {'day': 'tum'}}) is None + assert handler.wsock.close.called + assert handler.writer.close.called + handler.set_status.assert_called_once_with(201) + handler.write.assert_called_once_with({ + 'data': mock_file_metadata.json_api_serialized('3rqws') + }) @pytest.mark.asyncio - async def test_not_created(self): - metadata = mock.Mock() - self.mixin.resource = '3rqws' - self.mixin.uploader = asyncio.Future() - metadata.json_api_serialized.return_value = {'day': 'ta'} - self.mixin.uploader.set_result((metadata, False)) - - await self.mixin.upload_file() - - assert self.mixin.wsock.close.called - assert self.mixin.writer.close.called - assert self.mixin.set_status.called is False - assert self.mixin.write.assert_called_once_with({'data': {'day': 'ta'}}) is None + async def test_not_created(self, handler, mock_file_metadata): + handler.resource = '3rqws' + handler.uploader.set_result((mock_file_metadata, False)) + handler.set_status = mock.Mock() + + await handler.upload_file() + + assert handler.wsock.close.called + assert handler.writer.close.called + assert handler.set_status.called is False + handler.write.assert_called_once_with({ + 'data': mock_file_metadata.json_api_serialized('3rqws') + }) + diff --git a/tests/server/api/v1/test_metadata_mixin.py b/tests/server/api/v1/test_metadata_mixin.py index deca24fc2..d0386c878 100644 --- a/tests/server/api/v1/test_metadata_mixin.py +++ b/tests/server/api/v1/test_metadata_mixin.py @@ -1,128 +1,180 @@ +import json + import pytest -from unittest import mock -from waterbutler.server.api.v1.provider.metadata import MetadataMixin +from tests.utils import MockCoroutine +from waterbutler.core.path import WaterButlerPath +from tests.server.api.v1.fixtures import (http_request, handler, handler_auth, mock_stream, + mock_partial_stream, mock_file_metadata, + mock_folder_children, mock_revision_metadata) + + +class TestMetadataMixin: + + @pytest.mark.asyncio + async def test_header_file_metadata(self, handler, mock_file_metadata): + + handler.provider.metadata = MockCoroutine(return_value=mock_file_metadata) + + await handler.header_file_metadata() + + assert handler._headers['Content-Length'] == '1337' + assert handler._headers['Last-Modified'] == b'Wed, 25 Sep 1991 18:20:30 GMT' + assert handler._headers['Content-Type'] == b'application/octet-stream' + expected = bytes(json.dumps(mock_file_metadata.json_api_serialized(handler.resource)), + 'latin-1') + assert handler._headers['X-Waterbutler-Metadata'] == expected + + @pytest.mark.asyncio + async def test_get_folder(self, handler, mock_folder_children): + # The get_folder method expected behavior is to return folder children's metadata, not the + # metadata of the actual folder. This should be true of all providers. + + handler.provider.metadata = MockCoroutine(return_value=mock_folder_children) + + serialized_data = [x.json_api_serialized(handler.resource) for x in mock_folder_children] + await handler.get_folder() -class BaseMetadataMixinTest: + handler.write.assert_called_once_with({'data': serialized_data}) - def setup_method(self, method): - self.mixin = MetadataMixin() - self.mixin.write = mock.Mock() - self.mixin.request = mock.Mock() - self.mixin.set_status = mock.Mock() + @pytest.mark.asyncio + async def test_get_folder_download_as_zip(self, handler): + # Including 'zip' in the query params should trigger the download_as_zip method + handler.download_folder_as_zip = MockCoroutine() + handler.request.query_arguments['zip'] = '' -@pytest.mark.skipif -class TestHeaderMetadata(BaseMetadataMixinTest): + await handler.get_folder() - def test_revision(self): - pass + handler.download_folder_as_zip.assert_awaited_once() - def test_version(self): - pass + @pytest.mark.asyncio + async def test_get_file_metadata(self, handler): + handler.file_metadata = MockCoroutine() + handler.request.query_arguments['meta'] = '' - def test_size_none(self): - pass + await handler.get_file() - def test_modified_none(self): - pass + handler.file_metadata.assert_awaited_once() - def test_content_type_default(self): - pass + @pytest.mark.asyncio + @pytest.mark.parametrize('query_param', ['versions', 'revisions']) + async def test_get_file_versions(self, query_param, handler): + # Query parameters versions and revisions are equivalent, but versions is preferred for + # clarity. + handler.get_file_revisions = MockCoroutine() + handler.request.query_arguments[query_param] = '' - def test_x_waterbutler_metadata(self): - pass + await handler.get_file() + handler.get_file_revisions.assert_awaited_once() -@pytest.mark.skipif -class TestGetFolder(BaseMetadataMixinTest): + @pytest.mark.asyncio + async def test_get_file_download_file(self, handler): - def test_zip(self): - pass + handler.download_file = MockCoroutine() + await handler.get_file() - def test_listing(self): - pass + handler.download_file.assert_awaited_once() + @pytest.mark.asyncio + async def test_download_file_headers(self, handler, mock_stream): -@pytest.mark.skipif -class TestGetFile(BaseMetadataMixinTest): + handler.provider.download = MockCoroutine(return_value=mock_stream) + handler.path = WaterButlerPath('/test_file') - def test_meta(self): - pass + await handler.download_file() - def test_versions(self): - pass + assert handler._headers['Content-Length'] == bytes(str(mock_stream.size), 'latin-1') + assert handler._headers['Content-Type'] == bytes(mock_stream.content_type, 'latin-1') + assert handler._headers['Content-Disposition'] == bytes('attachment;filename="{}"'.format( + handler.path.name), 'latin-1') - def test_revisions(self): - pass + handler.write_stream.assert_awaited_once() - def test_download_file(self): - pass + @pytest.mark.asyncio + async def test_download_file_range_request_header(self, handler, mock_partial_stream): + handler.request.headers['Range'] = 'bytes=10-100' + handler.provider.download = MockCoroutine(return_value=mock_partial_stream) + handler.path = WaterButlerPath('/test_file') -@pytest.mark.skipif -class TestDownloadFile(BaseMetadataMixinTest): + await handler.download_file() - def test_range(self): - pass + assert handler._headers['Content-Range'] == bytes(mock_partial_stream.content_range, + 'latin-1') + assert handler.get_status() == 206 + handler.write_stream.assert_called_once_with(mock_partial_stream) - def test_version(self): - pass + @pytest.mark.asyncio + async def test_download_file_stream_redirect(self, handler): - def test_revision(self): - pass + handler.provider.download = MockCoroutine(return_value='stream') + await handler.download_file() - def test_mode(self): - pass + handler.redirect.assert_called_once_with('stream') - def test_direct(self): - pass + @pytest.mark.asyncio + @pytest.mark.parametrize("extension, mimetype", [ + ('.csv', 'text/csv'), + ('.md', 'text/x-markdown') + ]) + async def test_download_file_safari_mime_type(self, extension, mimetype, handler, mock_stream): + """ If the file extention is in mime_types override the content type to fix issues with + safari shoving in new file extensions """ - def test_redirect(self): - pass + handler.path = WaterButlerPath('/test_path.{}'.format(extension)) + handler.provider.download = MockCoroutine(return_value=mock_stream) - def test_content_type(self): - pass + await handler.download_file() - def test_content_length(self): - pass + handler.write_stream.assert_called_once_with(mock_stream) + assert handler._headers['Content-Type'] == bytes(mimetype, 'latin-1') - def test_display_name(self): - pass + @pytest.mark.asyncio + async def test_file_metadata(self, handler, mock_file_metadata): - def test_stream_name(self): - pass + handler.provider.metadata = MockCoroutine(return_value=mock_file_metadata) - def test_mime_type(self): - pass + await handler.file_metadata() + handler.write.assert_called_once_with({ + 'data': mock_file_metadata.json_api_serialized(handler.resource) + }) -@pytest.mark.skipif -class TestFileMetadata(BaseMetadataMixinTest): + @pytest.mark.asyncio + async def test_file_metadata_version(self, handler, mock_file_metadata): + handler.provider.metadata = MockCoroutine(return_value=mock_file_metadata) + handler.request.query_arguments['version'] = ['version id'] - def test_version(self): - pass + await handler.file_metadata() - def test_revision(self): - pass + handler.provider.metadata.assert_called_once_with(handler.path, revision='version id') + handler.write.assert_called_once_with({ + 'data': mock_file_metadata.json_api_serialized(handler.resource) + }) - def test_return(self): - pass + @pytest.mark.asyncio + async def test_get_file_revisions_raw(self, handler, mock_revision_metadata): + handler.provider.revisions = MockCoroutine(return_value=mock_revision_metadata) + await handler.get_file_revisions() -@pytest.mark.skipif -class TestFileRevisions(BaseMetadataMixinTest): + handler.write.assert_called_once_with({ + 'data': [r.json_api_serialized() for r in mock_revision_metadata] + }) - def test_return(self): - pass + @pytest.mark.asyncio + async def test_download_folder_as_zip(self, handler, mock_stream): - def test_not_coroutine(self): - pass + handler.provider.zip = MockCoroutine(return_value=mock_stream) + handler.path = WaterButlerPath('/test_file') + await handler.download_folder_as_zip() -@pytest.mark.skipif -class TestDownloadFolderAsZip(BaseMetadataMixinTest): + assert handler._headers['Content-Type'] == bytes('application/zip', 'latin-1') + expected = bytes('attachment;filename="{}"'.format(handler.path.name + '.zip'), 'latin-1') + assert handler._headers['Content-Disposition'] == expected - def test_return(self): - pass + handler.write_stream.assert_called_once_with(mock_stream) diff --git a/tests/server/api/v1/test_movecopy.py b/tests/server/api/v1/test_movecopy.py new file mode 100644 index 000000000..cd7b5e477 --- /dev/null +++ b/tests/server/api/v1/test_movecopy.py @@ -0,0 +1,133 @@ +import pytest + +from waterbutler.core import exceptions + +from tests.server.api.v1.fixtures import (http_request, handler, move_copy_args, handler_auth, + patch_auth_handler, serialized_request, + serialized_metadata, celery_src_copy_params, + celery_dest_copy_params, celery_dest_copy_params_root, + mock_intra, mock_inter, patch_make_provider_move_copy, + mock_file_metadata) + + +@pytest.mark.usefixtures('patch_auth_handler', 'patch_make_provider_move_copy') +class TestMoveOrCopy: + + def test_build_args(self, handler, move_copy_args): + assert handler.build_args() == move_copy_args + + @pytest.mark.asyncio + async def test_move_or_copy_invalid_action(self, handler): + handler._json = {'action': 'invalid'} + + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.move_or_copy() + + assert exc.value.message == 'Auth action must be "copy", "move", or "rename", not "invalid"' + + @pytest.mark.asyncio + async def test_move_or_copy_invalid_path(self, handler): + handler._json = {'action': 'copy'} + + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.move_or_copy() + + assert exc.value.message == '"path" field is required for moves or copies' + + @pytest.mark.asyncio + async def test_move_or_copy_invalid_path_slash(self, handler): + handler._json = {'action': 'copy', 'path': '/file'} + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.move_or_copy() + + assert exc.value.message == '"path" field requires a trailing' \ + ' slash to indicate it is a folder' + + @pytest.mark.asyncio + @pytest.mark.parametrize('action', ['move', 'copy']) + async def test_inter_move_copy(self, action, handler, mock_inter, mock_file_metadata, + serialized_metadata, celery_src_copy_params, + celery_dest_copy_params, serialized_request): + mock_make_provider, mock_celery = mock_inter + handler._json = {'action': action, 'path': '/test_path/'} + + await handler.move_or_copy() + + mock_make_provider.assert_called_with('MockProvider', + handler.auth['auth'], + handler.auth['credentials'], + handler.auth['settings']) + handler.write.assert_called_with(serialized_metadata) + assert handler.dest_meta == mock_file_metadata + mock_celery.assert_called_with(celery_src_copy_params, + celery_dest_copy_params, + conflict='warn', + rename=None, + request=serialized_request) + + @pytest.mark.asyncio + @pytest.mark.parametrize('action', ['move', 'copy']) + async def test_intra_move_copy(self, action, handler, mock_intra, serialized_metadata, + mock_file_metadata): + handler._json = {'action': action, 'path': '/test_path/'} + mock_make_provider, mock_celery = mock_intra + + await handler.move_or_copy() + + mock_make_provider.assert_called_with('MockProvider', + handler.auth['auth'], + handler.auth['credentials'], + handler.auth['settings']) + mock_celery.assert_called_with(getattr(handler.provider, action), + handler.provider, + handler.path, + handler.dest_path, + conflict='warn', + rename=None) + handler.write.assert_called_with(serialized_metadata) + assert handler.dest_meta == mock_file_metadata + + @pytest.mark.asyncio + async def test_invalid_rename(self, handler): + handler._json = {'action': 'rename', 'path': '/test_path/'} + + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.move_or_copy() + + assert exc.value.message == '"rename" field is required for renaming' + + @pytest.mark.asyncio + async def test_move_or_copy_invalid_rename_root(self, handler): + handler._json = {'action': 'copy', 'path': '/'} + handler.path = '/' + + with pytest.raises(exceptions.InvalidParameters) as exc: + await handler.move_or_copy() + + assert exc.value.message == '"rename" field is required for copying root' + + @pytest.mark.asyncio + async def test_rename(self, handler_auth, handler, mock_inter, mock_file_metadata, + serialized_metadata, celery_src_copy_params, + celery_dest_copy_params_root, serialized_request): + + mock_make_provider, mock_celery = mock_inter + handler._json = {'action': 'rename', 'rename': 'renamed path', 'path': '/test_path/'} + + await handler.move_or_copy() + + assert handler.dest_auth == handler_auth + assert handler.dest_path == handler.path.parent + assert handler.dest_resource == handler.resource + + mock_make_provider.assert_called_with('test', + handler.auth['auth'], + handler.auth['credentials'], + handler.auth['settings']) + handler.write.assert_called_with(serialized_metadata) + assert handler.dest_meta == mock_file_metadata + mock_celery.assert_called_with(celery_src_copy_params, + celery_dest_copy_params_root, + conflict='warn', + rename='renamed path', + request=serialized_request) diff --git a/tests/server/api/v1/test_provider.py b/tests/server/api/v1/test_provider.py new file mode 100644 index 000000000..fe9872e72 --- /dev/null +++ b/tests/server/api/v1/test_provider.py @@ -0,0 +1,225 @@ +from uuid import UUID +from unittest import mock + +import pytest + +from waterbutler.core.path import WaterButlerPath +from waterbutler.server.api.v1.provider import ProviderHandler, list_or_value + +from tests.utils import MockCoroutine, MockStream, MockWriter, MockProvider +from tests.server.api.v1.fixtures import (http_request, handler, patch_auth_handler, handler_auth, + patch_make_provider_core) + + +class TestUtils: + + def test_list_or_value(self): + with pytest.raises(AssertionError): + list_or_value('not list') + + assert list_or_value([]) is None + assert list_or_value([b'singleitem']) == 'singleitem' + assert list_or_value([b'decoded', b'value']) == ['decoded', 'value'] + + +class TestProviderHandler: + + @pytest.mark.asyncio + async def test_prepare(self, handler, patch_auth_handler, patch_make_provider_core): + await handler.prepare() + + # check that X-WATERBUTLER-REQUEST-ID is valid UUID + assert UUID(handler._headers['X-WATERBUTLER-REQUEST-ID'].decode('utf-8'), version=4) + + @pytest.mark.asyncio + async def test_prepare_put(self, handler, patch_auth_handler, patch_make_provider_core, + handler_auth): + handler.request.method = 'PUT' + handler.request.headers['Content-Length'] = 100 + await handler.prepare() + + assert handler.auth == handler_auth + assert handler.provider == MockProvider() + assert handler.path == WaterButlerPath('/file', prepend=None) + + # check that X-WATERBUTLER-REQUEST-ID is valid UUID + assert UUID(handler._headers['X-WATERBUTLER-REQUEST-ID'].decode('utf-8'), version=4) + + @pytest.mark.asyncio + async def test_prepare_stream(self, handler): + handler.target_path = WaterButlerPath('/file') + await handler.prepare_stream() + + @pytest.mark.asyncio + async def test_head(self, handler): + handler.path = WaterButlerPath('/file') + handler.header_file_metadata = MockCoroutine() + + await handler.head() + + handler.header_file_metadata.assert_called_with() + + @pytest.mark.asyncio + async def test_get_folder(self, handler): + handler.path = WaterButlerPath('/folder/') + handler.get_folder = MockCoroutine() + + await handler.get() + + handler.get_folder.assert_called_once_with() + + @pytest.mark.asyncio + async def test_get_file(self, handler): + handler.path = WaterButlerPath('/file') + handler.get_file = MockCoroutine() + + await handler.get() + + handler.get_file.assert_called_once_with() + + @pytest.mark.asyncio + async def test_put_file(self, handler): + handler.target_path = WaterButlerPath('/file') + handler.upload_file = MockCoroutine() + + await handler.put() + + handler.upload_file.assert_called_once_with() + + @pytest.mark.asyncio + async def test_put_folder(self, handler): + handler.target_path = WaterButlerPath('/folder/') + handler.create_folder = MockCoroutine() + + await handler.put() + + handler.create_folder.assert_called_once_with() + + @pytest.mark.asyncio + async def test_delete(self, handler): + handler.path = WaterButlerPath('/folder/') + handler.provider.delete = MockCoroutine() + + await handler.delete() + + handler.provider.delete.assert_called_once_with(WaterButlerPath('/folder/', prepend=None), + confirm_delete=0) + + @pytest.mark.asyncio + async def test_delete_confirm_delete(self, handler): + handler.path = WaterButlerPath('/folder/') + handler.provider.delete = MockCoroutine() + handler.request.query_arguments['confirm_delete'] = '1' + + await handler.delete() + + handler.provider.delete.assert_called_with(WaterButlerPath('/folder/', prepend=None), + confirm_delete=1) + + @pytest.mark.asyncio + async def test_data_received(self, handler): + handler.path = WaterButlerPath('/folder/') + handler.stream = None + handler.body = b'' + + await handler.data_received(b'1234567890') + + assert handler.bytes_uploaded == 10 + assert handler.body == b'1234567890' + + @pytest.mark.asyncio + async def test_data_received_stream(self, handler): + handler.path = WaterButlerPath('/folder/') + handler.stream = MockStream() + handler.writer = MockWriter() + + await handler.data_received(b'1234567890') + + assert handler.bytes_uploaded == 10 + handler.writer.write.assert_called_once_with(b'1234567890') + + @pytest.mark.asyncio + async def test_on_finish_download_file(self, handler): + handler.request.method = 'GET' + handler.path = WaterButlerPath('/file') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('download_file') + + @pytest.mark.asyncio + async def test_on_finish_download_zip(self, handler): + handler.request.method = 'GET' + handler.path = WaterButlerPath('/folder/') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('download_zip') + + @pytest.mark.asyncio + async def test_dont_send_hook_on_metadata(self, handler): + handler.request.query_arguments['meta'] = '' + handler.request.method = 'GET' + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + assert not handler._send_hook.called + + @pytest.mark.asyncio + async def test_on_finish_update(self, handler): + handler.request.method = 'PUT' + handler.path = WaterButlerPath('/file') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('update') + + @pytest.mark.asyncio + async def test_on_finish_create(self, handler): + handler.request.method = 'PUT' + handler._status_code = 201 + handler.target_path = WaterButlerPath('/file') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('create') + + @pytest.mark.asyncio + async def test_on_finish_create_folder(self, handler): + handler.request.method = 'PUT' + handler._status_code = 201 + handler.target_path = WaterButlerPath('/folder/') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('create_folder') + + @pytest.mark.asyncio + async def test_on_finish_move(self, handler): + handler.request.method = 'POST' + handler.body = b'{"action": "rename"}' + handler.target_path = WaterButlerPath('/folder/') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('move') + + @pytest.mark.asyncio + async def test_on_finish_copy(self, handler): + handler.request.method = 'POST' + handler.body = b'{"action": "copy"}' + handler.target_path = WaterButlerPath('/folder/') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('copy') + + @pytest.mark.asyncio + async def test_on_finish_delete(self, handler): + handler.request.method = 'DELETE' + handler.target_path = WaterButlerPath('/file') + handler._send_hook = mock.Mock() + + assert handler.on_finish() is None + handler._send_hook.assert_called_once_with('delete') + diff --git a/tests/utils.py b/tests/utils.py index e91b45a98..7d0e1db56 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,7 @@ from unittest import mock import pytest +import tornado from tornado import testing from tornado.platform.asyncio import AsyncIOMainLoop @@ -14,6 +15,7 @@ from waterbutler.core import provider from waterbutler.server.app import make_app from waterbutler.core.path import WaterButlerPath +from waterbutler.core.streams.file import FileStreamReader class MockCoroutine(mock.Mock): @@ -24,6 +26,9 @@ class MockCoroutine(mock.Mock): async def __call__(self, *args, **kwargs): return super().__call__(*args, **kwargs) + def assert_awaited_once(self): + assert self.call_count == 1 + class MockFileMetadata(metadata.BaseFileMetadata): provider = 'MockProvider' @@ -31,8 +36,8 @@ class MockFileMetadata(metadata.BaseFileMetadata): size = 1337 etag = 'etag' path = '/Foo.name' - modified = 'never' - modified_utc = 'never' + modified = '9/25/2017' + modified_utc = '1991-09-25T19:20:30.45+01:00' created_utc = 'always' content_type = 'application/octet-stream' @@ -64,6 +69,25 @@ def __init__(self): super().__init__({}) +class MockStream(FileStreamReader): + content_type = 'application/octet-stream' + size = 1334 + + def __init__(self): + super().__init__(tempfile.TemporaryFile()) + + +class MockRequestBody(tornado.concurrent.Future): + + def __await__(self): + yield None + + +class MockWriter(object): + write = mock.Mock() + drain = MockCoroutine() + + class MockProvider(provider.BaseProvider): NAME = 'MockProvider' copy = None @@ -85,8 +109,12 @@ def __init__(self, auth=None, creds=None, settings=None): self.upload = MockCoroutine() self.download = MockCoroutine() self.metadata = MockCoroutine() - self.validate_v1_path = MockCoroutine() - self.revalidate_path = MockCoroutine() + self.revalidate_path = MockCoroutine( + side_effect=lambda base, path, *args, **kwargs: base.child(path, *args, **kwargs)) + self.validate_v1_path = MockCoroutine( + side_effect=lambda path, **kwargs: WaterButlerPath(path, **kwargs)) + self.validate_path = MockCoroutine( + side_effect=lambda path, **kwargs: WaterButlerPath(path, **kwargs)) class MockProvider1(provider.BaseProvider): diff --git a/waterbutler/server/api/v1/provider/__init__.py b/waterbutler/server/api/v1/provider/__init__.py index a4cfeef2c..b31c9e6d0 100644 --- a/waterbutler/server/api/v1/provider/__init__.py +++ b/waterbutler/server/api/v1/provider/__init__.py @@ -26,7 +26,6 @@ def list_or_value(value): if len(value) == 0: return None if len(value) == 1: - # Remove leading slashes as they break things return value[0].decode('utf-8') return [item.decode('utf-8') for item in value]