Repository: dallinger/dallinger
Issue: info_post() route function raises error on Bartlett1932 Demo

<!--- Provide a general summary of the issue in the Title above -->

## Expected Behavior
Included demos should run without error in debug mode

## Current Behavior
Submitting the answer to a question on Bartlett1932 raises an exception:
```
17:14:01 web.1      |  [2017-12-15 17:14:01,060] ERROR in app: Exception on /info/2 [POST]
17:14:01 web.1      |  Traceback (most recent call last):
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask/app.py", line 1982, in wsgi_app
17:14:01 web.1      |      response = self.full_dispatch_request()
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask/app.py", line 1614, in full_dispatch_request
17:14:01 web.1      |      rv = self.handle_user_exception(e)
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask/app.py", line 1517, in handle_user_exception
17:14:01 web.1      |      reraise(exc_type, exc_value, tb)
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask/app.py", line 1612, in full_dispatch_request
17:14:01 web.1      |      rv = self.dispatch_request()
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask/app.py", line 1598, in dispatch_request
17:14:01 web.1      |      return self.view_functions[rule.endpoint](**req.view_args)
17:14:01 web.1      |    File "/Users/jesses/.virtualenvs/dallinger2/lib/python2.7/site-packages/flask_crossdomain.py", line 31, in wrapped_function
17:14:01 web.1      |      resp = make_response(f(*args, **kwargs))
17:14:01 web.1      |    File "/Users/jesses/py_dev/Dallinger2/dallinger/experiment_server/experiment_server.py", line 1050, in info_post
17:14:01 web.1      |      details = loads(details)
17:14:01 web.1      |    File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 339, in loads
17:14:01 web.1      |      return _default_decoder.decode(s)
17:14:01 web.1      |    File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 364, in decode
17:14:01 web.1      |      obj, end = self.raw_decode(s, idx=_w(s, 0).end())
17:14:01 web.1      |  TypeError: expected string or buffer
```

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. Run `dallinger debug --verbose` from `demos/dlgr/demos/bartlett1932`
2. Click through initial pages
3. Submit some text on the page with the heading "Now reproduce the passage, verbatim:"
4. Check console output

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Dallinger version used: master

## Notes
The route expects the `details` parameter to be present, but Bartlett1932 does not include this value when it calls `dallinger2.js.createInfo()` in its `experiment.js` file.

I'm unsure what should change here. Should the route accept posts without this value, or should clients always include it? If the latter, the route should check the return value of `request_parameter(parameter="details")` and raise an explicit error that way.

Changed files:
dallinger/experiment_server/experiment_server.py, tests/test_experiment_server.py

diff --git a/dallinger/experiment_server/experiment_server.py b/dallinger/experiment_server/experiment_server.py
index 303fea33..38b86cc9 100644
--- a/dallinger/experiment_server/experiment_server.py
+++ b/dallinger/experiment_server/experiment_server.py
@@ -1043,10 +1043,13 @@ def info_post(node_id):
     If info_type is a custom subclass of Info it must be
     added to the known_classes of the experiment class.
     """
-    details = request_parameter(parameter="details")
+    # get the parameters and validate them
+    contents = request_parameter(parameter="contents")
+    details = request_parameter(parameter="details", optional=True)
     if details:
         details = loads(details)
 
+    # Special case for TrackingEvents. TODO: moveme
     if request_parameter(parameter="info_type") == 'TrackingEvent':
         db.logger.debug('rq: Queueing %s with for node: %s for worker_function',
                         'TrackingEvent', node_id)
@@ -1054,24 +1057,21 @@ def info_post(node_id):
                   node_id=node_id, details=details)
         return success_response()
 
-    exp = Experiment(session)
-
-    # get the parameters
+    # Get info_type again, and this time, validate it along with the other
+    # parameters:
     info_type = request_parameter(parameter="info_type",
                                   parameter_type="known_class",
                                   default=models.Info)
-    contents = request_parameter(parameter="contents")
-    for x in [info_type, contents]:
+    for x in [contents, details, info_type]:
         if type(x) == Response:
             return x
 
-    if details:
-        details = loads(details)
     # check the node exists
     node = models.Node.query.get(node_id)
     if node is None:
         return error_response(error_type="/info POST, node does not exist")
 
+    exp = Experiment(session)
     try:
         # execute the request
         info = info_type(origin=node, contents=contents, details=details)
diff --git a/tests/test_experiment_server.py b/tests/test_experiment_server.py
index 87f74e6d..c103ce7e 100644
--- a/tests/test_experiment_server.py
+++ b/tests/test_experiment_server.py
@@ -691,6 +691,73 @@ class TestNodeRoutePOST(object):
         assert 'recipient Node does not exist' in data['html']
 
 
+@pytest.mark.usefixtures('experiment_dir', 'db_session')
+class TestInfoRoutePOST(object):
+
+    def test_invalid_node_id_returns_error(self, webapp):
+        nonexistent_node_id = 999
+        data = {'contents': 'foo'}
+        resp = webapp.post(
+            '/info/{}'.format(nonexistent_node_id),
+            data=data
+        )
+        data = json.loads(resp.data)
+        assert data['status'] == 'error'
+        assert 'node does not exist' in data['html']
+
+    def test_info_type_defaults_to_Info(self, a, webapp):
+        node = a.node()
+        data = {'contents': 'foo'}
+        resp = webapp.post(
+            '/info/{}'.format(node.id),
+            data=data
+        )
+        data = json.loads(resp.data)
+        assert u'info' in data
+
+    def test_queues_tracking_event(self, a, webapp):
+        node = a.node()
+        data = {'contents': 'foo', 'info_type': 'TrackingEvent'}
+        resp = webapp.post(
+            '/info/{}'.format(node.id),
+            data=data
+        )
+        data = json.loads(resp.data)
+        assert data == {u'status': u'success'}
+
+    def test_loads_details_json_value(self, a, webapp):
+        node = a.node()
+        data = {
+            'contents': 'foo',
+            'details': '{"key": "value"}'
+        }
+        resp = webapp.post(
+            '/info/{}'.format(node.id),
+            data=data
+        )
+        data = json.loads(resp.data)
+        assert data['info']['details'] == {u'key': u'value'}
+
+    def test_pings_experiment(self, a, webapp):
+        node = a.node()
+        data = {'contents': 'foo'}
+        with mock.patch('dallinger.experiment_server.experiment_server.Experiment') as mock_class:
+            mock_exp = mock.Mock(name="the experiment")
+            mock_class.return_value = mock_exp
+            webapp.post('/info/{}'.format(node.id), data=data)
+            mock_exp.info_post_request.assert_called_once()
+
+    def test_returns_error_if_experiment_ping_fails(self, a, webapp):
+        node = a.node()
+        data = {'contents': 'foo'}
+        with mock.patch('dallinger.experiment_server.experiment_server.Experiment') as mock_class:
+            mock_exp = mock.Mock(name="the experiment")
+            mock_exp.info_post_request.side_effect = Exception("boom!")
+            mock_class.return_value = mock_exp
+            resp = webapp.post('/info/{}'.format(node.id), data=data)
+        assert '/info POST server error' in resp.data
+
+
 @pytest.mark.usefixtures('experiment_dir')
 class TestNodeNeighbors(object):