Repository: cdent/gabbi

Issue:
Error when response content-type is application/jsonlines
If the response is `application/jsonlines` the JSON content handler kicks in and attempts to decode the response from json, which fails as it treats the whole doc as one single json object, not lines of json.

This is because the accept method for the handler is checking for a content type that starts with 'application/json'

So the simple fix is straightforward, but it might make better sense to create a jsonlines response handler and see if we can make a way to jsonpath on those lines.

Comments:

Patch:
diff --git a/gabbi/case.py b/gabbi/case.py
index 6b7823c..c1c0dec 100644
--- a/gabbi/case.py
+++ b/gabbi/case.py
@@ -595,8 +595,12 @@ class HTTPTestCase(unittest.TestCase):
                 data = self.replace_template(data)
                 data = dumper_class.dumps(data, test=self)
             else:
-                raise ValueError(
-                    'unable to process data to %s' % content_type)
+                if content_type:
+                    raise ValueError(
+                        'unable to process data to %s' % content_type)
+                else:
+                    raise ValueError(
+                        'no content-type available for processing data')
 
         data = self.replace_template(data)
 
diff --git a/gabbi/handlers/jsonhandler.py b/gabbi/handlers/jsonhandler.py
index 6647f45..6177779 100644
--- a/gabbi/handlers/jsonhandler.py
+++ b/gabbi/handlers/jsonhandler.py
@@ -38,9 +38,14 @@ class JSONHandler(base.ContentHandler):
 
     @staticmethod
     def accepts(content_type):
-        content_type = content_type.split(';', 1)[0].strip()
+        content_type = content_type.lower()
+        parameters = ''
+        if ';' in content_type:
+            content_type, parameters = content_type.split(';', 1)
+        content_type = content_type.strip()
         return (content_type.endswith('+json') or
-                content_type.startswith('application/json'))
+                content_type == 'application/json'
+                and 'stream=' not in parameters)
 
     @classmethod
     def replacer(cls, response_data, match):


Test patch:
diff --git a/gabbi/tests/gabbits_intercept/self.yaml b/gabbi/tests/gabbits_intercept/self.yaml
index bdd0705..14def1c 100644
--- a/gabbi/tests/gabbits_intercept/self.yaml
+++ b/gabbi/tests/gabbits_intercept/self.yaml
@@ -149,22 +149,6 @@ tests:
   response_json_paths:
       $.data[0]: hello
 
-- name: json home content type is json
-  url: /?data=hello
-  method: GET
-  request_headers:
-      accept: application/json-home
-  response_json_paths:
-      $.data[0]: hello
-
-- name: json content type detection case insensitive
-  url: /?data=hello
-  method: GET
-  request_headers:
-      accept: ApPlIcAtIoN/JsOn-hOmE
-  response_json_paths:
-      $.data[0]: hello
-
 - name: xml derived content type
   desc: +xml types should not work for json paths
   xfail: true
diff --git a/gabbi/tests/test_data_to_string.py b/gabbi/tests/test_data_to_string.py
new file mode 100644
index 0000000..d3150ba
--- /dev/null
+++ b/gabbi/tests/test_data_to_string.py
@@ -0,0 +1,54 @@
+#
+# 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 handling of data field in tests.
+"""
+
+import unittest
+
+from gabbi import case
+from gabbi import handlers
+
+
+class TestDataToString(unittest.TestCase):
+
+    def setUp(self):
+        self.case = case.HTTPTestCase('test_request')
+        self.case.content_handlers = []
+        for handler in handlers.RESPONSE_HANDLERS:
+            h = handler()
+            if hasattr(h, 'content_handler') and h.content_handler:
+                self.case.content_handlers.append(h)
+
+    def testHappyPath(self):
+        data = [{"hi": "low"}, {"yes": "no"}]
+        content_type = 'application/json'
+        body = self.case._test_data_to_string(data, content_type)
+        self.assertEqual('[{"hi": "low"}, {"yes": "no"}]', body)
+
+    def testNoContentType(self):
+        data = [{"hi": "low"}, {"yes": "no"}]
+        content_type = ''
+        with self.assertRaises(ValueError) as exc:
+            self.case._test_data_to_string(data, content_type)
+        self.assertEqual(
+            'no content-type available for processing data',
+            str(exc.exception))
+
+    def testNoHandler(self):
+        data = [{"hi": "low"}, {"yes": "no"}]
+        content_type = 'application/xml'
+        with self.assertRaises(ValueError) as exc:
+            self.case._test_data_to_string(data, content_type)
+        self.assertEqual(
+            'unable to process data to application/xml',
+            str(exc.exception))
diff --git a/gabbi/tests/test_handlers.py b/gabbi/tests/test_handlers.py
index b07e73f..17c753e 100644
--- a/gabbi/tests/test_handlers.py
+++ b/gabbi/tests/test_handlers.py
@@ -444,3 +444,40 @@ class HandlersTest(unittest.TestCase):
         # method and then run its tests to confirm.
         test = self.test('test_request')
         handler(test)
+
+
+class TestJSONHandlerAccept(unittest.TestCase):
+    """Test that the json handler accepts function.
+
+    We need to confirm that it returns True and False at the right
+    times. This is somewhat tricky as there are a fair number of
+    MIME-types that include the string "JSON" but aren't, as a
+    whole document, capable of being decoded.
+    """
+
+    def _test_content_type(self, content_type, expected):
+        if expected:
+            self.assertTrue(
+                jsonhandler.JSONHandler.accepts(content_type),
+                "expected %s to be accepted but it was not!" % content_type)
+        else:
+            self.assertFalse(
+                jsonhandler.JSONHandler.accepts(content_type),
+                "expected %s to not be accepted but it was!" % content_type)
+
+    def test_many_content_types(self):
+        cases = [
+            ("application/json", True),
+            ("application/JSON", True),
+            ("text/plain", False),
+            ("application/jsonlines", False),
+            ("application/json;stream=true", False),
+            ("application/json;streamable=pony", True),
+            ("application/stream+json", True),
+            ("application/xml", False),
+            ("application/json-seq", False),
+            ("application/json-home", False),
+        ]
+        for test in cases:
+            with self.subTest(test[0]):
+                self._test_content_type(*test)