import ntpath import os import sys import tempfile import unittest try: from unittest.mock import Mock, patch, call, mock_open except ImportError: from mock import Mock, patch, call, mock_open from flask import Flask, render_template_string, Blueprint import six import flask_s3 from flask_s3 import FlaskS3 class FlaskStaticTest(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True @self.app.route('/<url_for_string>') def a(url_for_string): return render_template_string(url_for_string) def test_jinja_url_for(self): """ Tests that the jinja global gets assigned correctly. """ self.assertNotEqual(self.app.jinja_env.globals['url_for'], flask_s3.url_for) # then we initialise the extension FlaskS3(self.app) self.assertEquals(self.app.jinja_env.globals['url_for'], flask_s3.url_for) # Temporarily commented out """ def test_config(self): "" Tests configuration vars exist. "" FlaskS3(self.app) defaults = ('S3_USE_HTTP', 'USE_S3', 'USE_S3_DEBUG', 'S3_BUCKET_DOMAIN', 'S3_CDN_DOMAIN', 'S3_USE_CACHE_CONTROL', 'S3_HEADERS', 'S3_URL_STYLE') for default in defaults: self.assertIn(default, self.app.config) """ class UrlTests(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True self.app.config['FLASKS3_BUCKET_NAME'] = 'foo' self.app.config['FLASKS3_USE_HTTPS'] = True self.app.config['FLASKS3_BUCKET_DOMAIN'] = 's3.amazonaws.com' self.app.config['FLASKS3_CDN_DOMAIN'] = '' self.app.config['FLASKS3_OVERRIDE_TESTING'] = True @self.app.route('/<url_for_string>') def a(url_for_string): return render_template_string(url_for_string) @self.app.route('/') def b(): return render_template_string("{{url_for('b')}}") bp = Blueprint('admin', __name__, static_folder='admin-static') @bp.route('/<url_for_string>') def c(): return render_template_string("{{url_for('b')}}") self.app.register_blueprint(bp) def client_get(self, ufs): FlaskS3(self.app) client = self.app.test_client() import six if six.PY3: return client.get('/%s' % ufs) elif six.PY2: return client.get('/{}'.format(ufs)) def test_required_config(self): """ Tests that ValueError raised if bucket address not provided. """ raises = False del self.app.config['FLASKS3_BUCKET_NAME'] try: ufs = "{{url_for('static', filename='bah.js')}}" self.client_get(ufs) except ValueError: raises = True self.assertTrue(raises) def test_url_for(self): """ Tests that correct url formed for static asset in self.app. """ # non static endpoint url_for in template self.assertEquals(self.client_get('').data, six.b('/')) # static endpoint url_for in template ufs = "{{url_for('static', filename='bah.js')}}" exp = 'https://foo.s3.amazonaws.com/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_per_url_scheme(self): """ Tests that if _scheme is passed in the url_for arguments, that scheme is used instead of configuration scheme. """ # check _scheme overriden per url ufs = "{{url_for('static', filename='bah.js', _scheme='http')}}" exp = 'http://foo.s3.amazonaws.com/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_handles_special_args(self): """ Tests that if any special arguments are passed, they are ignored, and removed from generated url. As of this writing these are the special args: _external, _anchor, _method (from flask's url_for) """ # check _external, _anchor, and _method are ignored, and not added # to the url ufs = "{{url_for('static', filename='bah.js',\ _external=True, _anchor='foobar', _method='GET')}}" exp = 'https://foo.s3.amazonaws.com/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_debug(self): """Tests Flask-S3 behaviour in debug mode.""" self.app.debug = True # static endpoint url_for in template ufs = "{{url_for('static', filename='bah.js')}}" exp = '/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_debug_override(self): """Tests Flask-S3 behavior in debug mode with USE_S3_DEBUG turned on.""" self.app.debug = True self.app.config['FLASKS3_DEBUG'] = True ufs = "{{url_for('static', filename='bah.js')}}" exp = 'https://foo.s3.amazonaws.com/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_blueprint(self): """ Tests that correct url formed for static asset in blueprint. """ # static endpoint url_for in template ufs = "{{url_for('admin.static', filename='bah.js')}}" exp = 'https://foo.s3.amazonaws.com/admin-static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_cdn_domain(self): self.app.config['FLASKS3_CDN_DOMAIN'] = 'foo.cloudfront.net' ufs = "{{url_for('static', filename='bah.js')}}" exp = 'https://foo.cloudfront.net/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_url_style_path(self): """Tests that the URL returned uses the path style.""" self.app.config['FLASKS3_URL_STYLE'] = 'path' ufs = "{{url_for('static', filename='bah.js')}}" exp = 'https://s3.amazonaws.com/foo/static/bah.js' self.assertEquals(self.client_get(ufs).data, six.b(exp)) def test_url_for_url_style_invalid(self): """Tests that an exception is raised for invalid URL styles.""" self.app.config['FLASKS3_URL_STYLE'] = 'balderdash' ufs = "{{url_for('static', filename='bah.js')}}" self.assertRaises(ValueError, self.client_get, six.b(ufs)) class S3TestsWithCustomEndpoint(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True self.app.config['FLASKS3_BUCKET_NAME'] = 'thebucket' self.app.config['FLASKS3_REGION'] = 'theregion' self.app.config['AWS_ACCESS_KEY_ID'] = 'thekeyid' self.app.config['AWS_SECRET_ACCESS_KEY'] = 'thesecretkey' self.app.config['FLASKS3_ENDPOINT_URL'] = 'https://minio.local:9000/' @patch('flask_s3.boto3') def test__custom_endpoint_is_passed_to_boto(self, mock_boto3): flask_s3.create_all(self.app) mock_boto3.client.assert_called_once_with("s3", region_name='theregion', aws_access_key_id='thekeyid', aws_secret_access_key='thesecretkey', endpoint_url='https://minio.local:9000/') class S3Tests(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.testing = True self.app.config['FLASKS3_BUCKET_NAME'] = 'foo' self.app.config['FLASKS3_USE_CACHE_CONTROL'] = True self.app.config['FLASKS3_CACHE_CONTROL'] = 'cache instruction' self.app.config['FLASKS3_CACHE_CONTROL'] = '3600' self.app.config['FLASKS3_HEADERS'] = { 'Expires': 'Thu, 31 Dec 2037 23:59:59 GMT', 'Content-Encoding': 'gzip', } self.app.config['FLASKS3_ONLY_MODIFIED'] = False def test__bp_static_url(self): """ Tests test__bp_static_url """ bps = [Mock(static_url_path='/foo', url_prefix=None), Mock(static_url_path=None, url_prefix='/pref'), Mock(static_url_path='/b/bar', url_prefix='/pref'), Mock(static_url_path=None, url_prefix=None)] expected = [six.u('/foo'), six.u('/pref'), six.u('/pref/b/bar'), six.u('')] self.assertEquals(expected, [flask_s3._bp_static_url(x) for x in bps]) def test__cache_config(self): """ Test that cache headers are set correctly. """ new_app = Flask("test_cache_param") new_app.config['FLASKS3_USE_CACHE_CONTROL'] = True new_app.config['FLASKS3_CACHE_CONTROL'] = '3600' flask_s3.FlaskS3(new_app) expected = {'Cache-Control': '3600'} self.assertEqual(expected, new_app.config['FLASKS3_HEADERS']) @patch('os.walk') @patch('os.path.isdir') def test__gather_files(self, path_mock, os_mock): """ Tests the _gather_files function """ self.app.static_folder = '/home' self.app.static_url_path = '/static' bp_a = Mock(static_folder='/home/bar', static_url_path='/a/bar', url_prefix=None) bp_b = Mock(static_folder='/home/zoo', static_url_path='/b/bar', url_prefix=None) bp_c = Mock(static_folder=None) self.app.blueprints = {'a': bp_a, 'b': bp_b, 'c': bp_c} dirs = {'/home': [('/home', None, ['.a'])], '/home/bar': [('/home/bar', None, ['b'])], '/home/zoo': [('/home/zoo', None, ['c']), ('/home/zoo/foo', None, ['d', 'e'])]} os_mock.side_effect = dirs.get path_mock.return_value = True expected = {('/home/bar', six.u('/a/bar')): ['/home/bar/b'], ('/home/zoo', six.u('/b/bar')): ['/home/zoo/c', '/home/zoo/foo/d', '/home/zoo/foo/e']} actual = flask_s3._gather_files(self.app, False) self.assertEqual(expected, actual) expected[('/home', six.u('/static'))] = ['/home/.a'] actual = flask_s3._gather_files(self.app, True) self.assertEqual(expected, actual) @patch('os.walk') @patch('os.path.isdir') def test__gather_files_no_blueprints_no_files(self, path_mock, os_mock): """ Tests that _gather_files works when there are no blueprints and no files available in the static folder """ self.app.static_folder = '/foo' dirs = {'/foo': [('/foo', None, [])]} os_mock.side_effect = dirs.get path_mock.return_value = True actual = flask_s3._gather_files(self.app, False) self.assertEqual({}, actual) @patch('os.walk') @patch('os.path.isdir') def test__gather_files_bad_folder(self, path_mock, os_mock): """ Tests that _gather_files when static folder is not valid folder """ self.app.static_folder = '/bad' dirs = {'/bad': []} os_mock.side_effect = dirs.get path_mock.return_value = False actual = flask_s3._gather_files(self.app, False) self.assertEqual({}, actual) @patch('os.path.splitdrive', side_effect=ntpath.splitdrive) @patch('os.path.join', side_effect=ntpath.join) def test__path_to_relative_url_win(self, join_mock, split_mock): """ Tests _path_to_relative_url on Windows system """ input_ = [r'C:\foo\bar\baz.css', r'C:\foo\bar.css', r'\foo\bar.css'] expected = ['/foo/bar/baz.css', '/foo/bar.css', '/foo/bar.css'] for in_, exp in zip(input_, expected): actual = flask_s3._path_to_relative_url(in_) self.assertEquals(exp, actual) @unittest.skipIf(sys.version_info < (3, 0), "not supported in this version") @patch('flask_s3.boto3') @patch("{}.open".format("builtins"), mock_open(read_data='test')) def test__write_files(self, key_mock): """ Tests _write_files """ static_url_loc = '/foo/static' static_folder = '/home/z' assets = ['/home/z/bar.css', '/home/z/foo.css'] exclude = ['/foo/static/foo.css', '/foo/static/foo/bar.css'] # we expect foo.css to be excluded and not uploaded expected = [call(bucket=None, name=six.u('/foo/static/bar.css')), call().set_metadata('Cache-Control', 'cache instruction'), call().set_metadata('Expires', 'Thu, 31 Dec 2037 23:59:59 GMT'), call().set_metadata('Content-Encoding', 'gzip'), call().set_contents_from_filename('/home/z/bar.css')] flask_s3._write_files(key_mock, self.app, static_url_loc, static_folder, assets, None, exclude) self.assertLessEqual(expected, key_mock.mock_calls) @patch('flask_s3.boto3') def test__write_only_modified(self, key_mock): """ Test that we only upload files that have changed """ self.app.config['FLASKS3_ONLY_MODIFIED'] = True static_folder = tempfile.mkdtemp() static_url_loc = static_folder filenames = [os.path.join(static_folder, f) for f in ['foo.css', 'bar.css']] expected = [] def IntIterator(): i = 0 while True: i += 1 yield i data_iter = IntIterator() for filename in filenames: # Write random data into files with open(filename, 'wb') as f: if six.PY3: data = str(data_iter) f.write(data.encode()) else: data = str(data_iter.next()) f.write(data) # We expect each file to be uploaded expected.append(call.put_object(ACL='public-read', Bucket=None, Key=filename.lstrip("/"), Body=data, Metadata={}, Expires='Thu, 31 Dec 2037 23:59:59 GMT', ContentEncoding='gzip')) files = {(static_url_loc, static_folder): filenames} hashes = flask_s3._upload_files(key_mock, self.app, files, None) # All files are uploaded and hashes are returned self.assertLessEqual(len(expected), len(key_mock.mock_calls)) self.assertEquals(len(hashes), len(filenames)) # We now modify the second file with open(filenames[1], 'wb') as f: data = str(next(data_iter)) if six.PY2: f.write(data) else: f.write(data.encode()) # We expect only this file to be uploaded expected.append(call.put_object(ACL='public-read', Bucket=None, Key=filenames[1].lstrip("/"), Body=data, Metadata={}, Expires='Thu, 31 Dec 2037 23:59:59 GMT', ContentEncoding='gzip')) new_hashes = flask_s3._upload_files(key_mock, self.app, files, None, hashes=dict(hashes)) #import pprint #pprint.pprint(zip(expected, key_mock.mock_calls)) self.assertEquals(len(expected), len(key_mock.mock_calls)) @patch('flask_s3.boto3') def test_write_binary_file(self, key_mock): """ Tests _write_files """ self.app.config['FLASKS3_ONLY_MODIFIED'] = True static_folder = tempfile.mkdtemp() static_url_loc = static_folder filenames = [os.path.join(static_folder, 'favicon.ico')] for filename in filenames: # Write random data into files with open(filename, 'wb') as f: f.write(bytearray([120, 3, 255, 0, 100])) flask_s3._write_files(key_mock, self.app, static_url_loc, static_folder, filenames, None) expected = { 'ACL': 'public-read', 'Bucket': None, 'Metadata': {}, 'ContentEncoding': 'gzip', 'Body': b'x\x03\xff\x00d', 'Key': filenames[0][1:], 'Expires': 'Thu, 31 Dec 2037 23:59:59 GMT'} name, args, kwargs = key_mock.mock_calls[0] self.assertEquals(expected, kwargs) def test_static_folder_path(self): """ Tests _static_folder_path """ inputs = [('/static', '/home/static', '/home/static/foo.css'), ('/foo/static', '/home/foo/s', '/home/foo/s/a/b.css'), ('/bar/', '/bar/', '/bar/s/a/b.css')] expected = [six.u('/static/foo.css'), six.u('/foo/static/a/b.css'), six.u('/bar/s/a/b.css')] for i, e in zip(inputs, expected): self.assertEquals(e, flask_s3._static_folder_path(*i)) @patch('flask_s3.boto3') def test__bucket_acl_not_set(self, mock_boto3): flask_s3.create_all(self.app, put_bucket_acl=False) self.assertFalse(mock_boto3.client().put_bucket_acl.called, "put_bucket_acl was called!") @patch('flask_s3._write_files') def test__upload_uses_prefix(self, mock_write_files): s3_mock = Mock() local_path = '/local_path/static' file_paths = ['/local_path/static/file1', '/local_path/static/file2'] files = {(local_path, '/static'): file_paths} flask_s3._upload_files(s3_mock, self.app, files, 's3_bucket') expected_call = call( s3_mock, self.app, '/static', local_path, file_paths, 's3_bucket', hashes=None) self.assertEquals(mock_write_files.call_args_list, [expected_call]) for supported_prefix in ['foo', '/foo', 'foo/', '/foo/']: mock_write_files.reset_mock() self.app.config['FLASKS3_PREFIX'] = supported_prefix flask_s3._upload_files(s3_mock, self.app, files, 's3_bucket') expected_call = call(s3_mock, self.app, '/foo/static', local_path, file_paths, 's3_bucket', hashes=None) self.assertEquals(mock_write_files.call_args_list, [expected_call]) @patch('flask_s3.current_app') def test__url_for_uses_prefix(self, mock_current_app): bucket_path = 'foo.s3.amazonaws.com' flask_s3.FlaskS3(self.app) mock_current_app.config = self.app.config mock_bind = mock_current_app.url_map.bind flask_s3.url_for('static', **{'filename': 'test_file.txt'}) self.assertEqual(mock_bind.call_args_list, [call(bucket_path, url_scheme='https')]) for supported_prefix in ['bar', '/bar', 'bar/', '/bar/']: mock_bind.reset_mock() self.app.config['FLASKS3_PREFIX'] = supported_prefix flask_s3.url_for('static', **{'filename': 'test_file.txt'}) expected_path = '%s/%s' % (bucket_path, 'bar') self.assertEqual(mock_bind.call_args_list, [call(expected_path, url_scheme='https')]) if __name__ == '__main__': unittest.main()