diff --git a/docs/source/format.rst b/docs/source/format.rst index cc1b129..51dcf24 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -269,8 +269,9 @@ flexibility when doing a ``POST`` or ``PUT``. If the value is not a string (that is, it is a sequence or structure) it is treated as a data structure which is turned into a JSON string. If the value is a string that begins with ``<@`` then the rest of the string is treated -as the name of a file to be loaded from the same directory as the YAML -file. If the value is an undecorated string, that's the value. +as a filepath to be loaded. The path is relative to the test directory +and may not traverse up into parent directories. If the value is an +undecorated string, that's the value. When reading from a file care should be taken to ensure that a reasonable content-type is set for the data as this will control if any diff --git a/docs/source/runner.rst b/docs/source/runner.rst index df17db4..a468bf6 100644 --- a/docs/source/runner.rst +++ b/docs/source/runner.rst @@ -50,3 +50,6 @@ YAML will default to ``ssl: True``. If a ``-x`` or ``--failfast`` argument is provided then ``gabbi-run`` will exit after the first test failure. + +Use ``-v`` or ``--verbose`` with a value of ``all``, ``headers`` or ``body`` +to turn on :ref:`verbosity ` for all tests being run. diff --git a/gabbi/case.py b/gabbi/case.py index 63dfb82..f145e7d 100644 --- a/gabbi/case.py +++ b/gabbi/case.py @@ -243,7 +243,13 @@ class HTTPTestCase(testtools.TestCase): def _load_data_file(self, filename): """Read a file from the current test directory.""" - path = os.path.join(self.test_directory, os.path.basename(filename)) + path = os.path.join(self.test_directory, filename) + has_dir_traversal = os.path.relpath( + path, start=self.test_directory).startswith(os.pardir) + if has_dir_traversal: + raise ValueError( + 'Attempted loading of data file outside test directory: %s' + % filename) with open(path, mode='rb') as data_file: return data_file.read() diff --git a/gabbi/runner.py b/gabbi/runner.py index ac98dea..c1210c8 100644 --- a/gabbi/runner.py +++ b/gabbi/runner.py @@ -14,6 +14,7 @@ import argparse from importlib import import_module +import os import sys import unittest @@ -57,6 +58,9 @@ def run(): gabbi-run -x example.com:9999 /mountpoint < mytest.yaml + Use `-v` or `--verbose` with a value of `all`, `headers` or `body` to + turn on verbosity for all tests being run. + Multiple files may be named as arguments, separated from other arguments by a ``--``. Each file will be run as a separate test suite:: @@ -74,18 +78,21 @@ def run(): handler_objects = initialize_handlers(args.response_handlers) + verbosity = args.verbosity failfast = args.failfast failure = False if not input_files: success = run_suite(sys.stdin, handler_objects, host, port, - prefix, force_ssl, failfast) + prefix, force_ssl, failfast, verbosity) failure = not success else: for input_file in input_files: with open(input_file, 'r') as fh: + data_dir = os.path.dirname(input_file) success = run_suite(fh, handler_objects, host, port, - prefix, force_ssl, failfast) + prefix, force_ssl, failfast, data_dir, + verbosity) if not failure: # once failed, this is considered immutable failure = not success if failure and failfast: @@ -95,7 +102,7 @@ def run(): def run_suite(handle, handler_objects, host, port, prefix, force_ssl=False, - failfast=False): + failfast=False, data_dir='.', verbosity=False): """Run the tests from the YAML in handle.""" data = utils.load_yaml(handle) if force_ssl: @@ -103,10 +110,15 @@ def run_suite(handle, handler_objects, host, port, prefix, force_ssl=False, data['defaults']['ssl'] = True else: data['defaults'] = {'ssl': True} + if verbosity: + if 'defaults' in data: + data['defaults']['verbose'] = verbosity + else: + data['defaults'] = {'verbose': verbosity} loader = unittest.defaultTestLoader test_suite = suitemaker.test_suite_from_dict( - loader, 'input', data, '.', host, port, None, None, prefix=prefix, + loader, 'input', data, data_dir, host, port, None, None, prefix=prefix, handlers=handler_objects) result = ConciseTestRunner( @@ -194,6 +206,12 @@ def _make_argparser(): help='Custom response handler. Should be an import path of the ' 'form package.module or package.module:class.' ) + parser.add_argument( + '-v', '--verbose', + dest='verbosity', + choices=['all', 'body', 'headers'], + help='Turn on test verbosity for all tests run in this session.' + ) return parser diff --git a/gabbi/tests/gabbits_runner/subdir/sample.json b/gabbi/tests/gabbits_runner/subdir/sample.json new file mode 100644 index 0000000..ddbce20 --- /dev/null +++ b/gabbi/tests/gabbits_runner/subdir/sample.json @@ -0,0 +1 @@ +{"items": {"house": "blue"}} diff --git a/gabbi/tests/gabbits_runner/test_data.yaml b/gabbi/tests/gabbits_runner/test_data.yaml new file mode 100644 index 0000000..35d056a --- /dev/null +++ b/gabbi/tests/gabbits_runner/test_data.yaml @@ -0,0 +1,8 @@ +tests: + +- name: POST data from file + verbose: true + POST: / + request_headers: + content-type: application/json + data: <@subdir/sample.json diff --git a/gabbi/tests/gabbits_runner/verbosity.yaml b/gabbi/tests/gabbits_runner/verbosity.yaml new file mode 100644 index 0000000..4522211 --- /dev/null +++ b/gabbi/tests/gabbits_runner/verbosity.yaml @@ -0,0 +1,8 @@ +tests: + +- name: simple data post + POST: / + request_headers: + content-type: application/json + data: + cat: poppy diff --git a/gabbi/tests/test_load_data_file.py b/gabbi/tests/test_load_data_file.py new file mode 100644 index 0000000..99536d4 --- /dev/null +++ b/gabbi/tests/test_load_data_file.py @@ -0,0 +1,77 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Test loading data from files with <@. +""" + +import unittest + +from gabbi import case +from six.moves import mock + + +@mock.patch( + 'gabbi.case.open', + new_callable=mock.mock_open, + read_data='dummy content', + create=True, +) +class DataFileTest(unittest.TestCase): + """Reading from local file is only allowed at or below the + test_directory level. + """ + + def setUp(self): + self.http_case = case.HTTPTestCase('test_request') + + def _assert_content_read(self, filepath): + self.assertEqual( + 'dummy content', self.http_case._load_data_file(filepath)) + + def test_load_file(self, m_open): + self.http_case.test_directory = '.' + self._assert_content_read('data.json') + m_open.assert_called_with('./data.json', mode='rb') + + def test_load_file_in_directory(self, m_open): + self.http_case.test_directory = '.' + self._assert_content_read('a/b/c/data.json') + m_open.assert_called_with('./a/b/c/data.json', mode='rb') + + def test_load_file_in_root(self, m_open): + self.http_case.test_directory = '.' + filepath = '/top-level.private' + + with self.assertRaises(ValueError): + self.http_case._load_data_file(filepath) + self.assertFalse(m_open.called) + + def test_load_file_in_parent_dir(self, m_open): + self.http_case.test_directory = '.' + filepath = '../file-in-parent-dir.txt' + + with self.assertRaises(ValueError): + self.http_case._load_data_file(filepath) + self.assertFalse(m_open.called) + + def test_load_file_within_test_directory(self, m_open): + self.http_case.test_directory = '/a/b/c' + self._assert_content_read('../../b/c/file-in-test-dir.txt') + m_open.assert_called_with( + '/a/b/c/../../b/c/file-in-test-dir.txt', mode='rb') + + def test_load_file_not_within_test_directory(self, m_open): + self.http_case.test_directory = '/a/b/c' + filepath = '../../b/E/file-in-test-dir.txt' + with self.assertRaises(ValueError): + self.http_case._load_data_file(filepath) + self.assertFalse(m_open.called) diff --git a/gabbi/tests/test_runner.py b/gabbi/tests/test_runner.py index bf882ab..d77b9e0 100644 --- a/gabbi/tests/test_runner.py +++ b/gabbi/tests/test_runner.py @@ -22,6 +22,7 @@ from wsgi_intercept.interceptor import Urllib3Interceptor from gabbi import exception from gabbi.handlers import base +from gabbi.handlers.jsonhandler import JSONHandler from gabbi import runner from gabbi.tests.simple_wsgi import SimpleWsgi @@ -249,6 +250,77 @@ class RunnerTest(unittest.TestCase): self.assertIn('{\n', output) self.assertIn('}\n', output) + def test_data_dir_good(self): + """Confirm that data dir is the test file's dir.""" + sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] + + sys.argv.append('--') + sys.argv.append('gabbi/tests/gabbits_runner/test_data.yaml') + + with self.server(): + try: + runner.run() + except SystemExit as err: + self.assertSuccess(err) + + # Compare the verbose output of tests with pretty printed + # data. + with open('gabbi/tests/gabbits_runner/subdir/sample.json') as data: + data = JSONHandler.loads(data.read()) + expected_string = JSONHandler.dumps(data, pretty=True) + + sys.stdout.seek(0) + output = sys.stdout.read() + self.assertIn(expected_string, output) + + def _run_verbosity_arg(self): + sys.argv.append('--') + sys.argv.append('gabbi/tests/gabbits_runner/verbosity.yaml') + + with self.server(): + try: + runner.run() + except SystemExit as err: + self.assertSuccess(err) + + sys.stdout.seek(0) + output = sys.stdout.read() + return output + + def test_verbosity_arg_none(self): + """Confirm --verbose handling.""" + sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] + + output = self._run_verbosity_arg() + self.assertEqual('', output) + + def test_verbosity_arg_body(self): + """Confirm --verbose handling.""" + sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), + '--verbose=body'] + + output = self._run_verbosity_arg() + self.assertIn('{\n "cat": "poppy"\n}', output) + self.assertNotIn('application/json', output) + + def test_verbosity_arg_headers(self): + """Confirm --verbose handling.""" + sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), + '--verbose=headers'] + + output = self._run_verbosity_arg() + self.assertNotIn('{\n "cat": "poppy"\n}', output) + self.assertIn('application/json', output) + + def test_verbosity_arg_all(self): + """Confirm --verbose handling.""" + sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), + '--verbose=all'] + + output = self._run_verbosity_arg() + self.assertIn('{\n "cat": "poppy"\n}', output) + self.assertIn('application/json', output) + def assertSuccess(self, exitError): errors = exitError.args[0] if errors: